keycloak-uncached
Changes
distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli 7(+7 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli 7(+7 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli 7(+7 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli 7(+7 -0)
services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java 31(+22 -9)
services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java 140(+140 -0)
services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookupFactory.java 76(+76 -0)
services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookup.java 61(+61 -0)
services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookupFactory.java 43(+43 -0)
services/src/main/java/org/keycloak/services/x509/DefaultClientCertificateLookupFactory.java 66(+66 -0)
services/src/main/java/org/keycloak/services/x509/HaProxySslClientCertificateLookupFactory.java 42(+42 -0)
services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookupFactory.java 31(+31 -0)
services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory 21(+21 -0)
Details
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli
index eb8e567..3498794 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli
@@ -432,4 +432,11 @@ if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-
echo
end-if
+if (outcome == failed) of /profile=$clusteredProfile/subsystem=keycloak-server/spi=x509cert-lookup/:read-resource
+ echo Adding spi=x509cert-lookup...
+ /profile=$clusteredProfile/subsystem=keycloak-server/spi=x509cert-lookup/:add(default-provider=${keycloak.x509cert.lookup.provider:default})
+ /profile=$clusteredProfile/subsystem=keycloak-server/spi=x509cert-lookup/provider=default/:add(enabled=true)
+ echo
+end-if
+
echo *** End Migration of /profile=$clusteredProfile ***
\ No newline at end of file
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli
index 1027d17..33a2334 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli
@@ -389,4 +389,11 @@ if (outcome == failed) of /profile=$standaloneProfile/subsystem=infinispan/cache
echo
end-if
+if (outcome == failed) of /profile=$standaloneProfile/subsystem=keycloak-server/spi=x509cert-lookup/:read-resource
+ echo Adding spi=x509cert-lookup...
+ /profile=$standaloneProfile/subsystem=keycloak-server/spi=x509cert-lookup/:add(default-provider=${keycloak.x509cert.lookup.provider:default})
+ /profile=$standaloneProfile/subsystem=keycloak-server/spi=x509cert-lookup/provider=default/:add(enabled=true)
+ echo
+end-if
+
echo *** End Migration of /profile=$standaloneProfile ***
\ No newline at end of file
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli
index 5d688dd..9bf0b42 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli
@@ -360,4 +360,11 @@ if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-c
echo
end-if
+if (outcome == failed) of /subsystem=keycloak-server/spi=x509cert-lookup/:read-resource
+ echo Adding spi=x509cert-lookup...
+ /subsystem=keycloak-server/spi=x509cert-lookup/:add(default-provider=${keycloak.x509cert.lookup.provider:default})
+ /subsystem=keycloak-server/spi=x509cert-lookup/provider=default/:add(enabled=true)
+ echo
+end-if
+
echo *** End Migration ***
\ No newline at end of file
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli
index 767ff18..aea4a6f 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli
@@ -416,4 +416,11 @@ if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distrib
echo
end-if
+if (outcome == failed) of /subsystem=keycloak-server/spi=x509cert-lookup/:read-resource
+ echo Adding spi=x509cert-lookup...
+ /subsystem=keycloak-server/spi=x509cert-lookup/:add(default-provider=${keycloak.x509cert.lookup.provider:default})
+ /subsystem=keycloak-server/spi=x509cert-lookup/provider=default/:add(enabled=true)
+ echo
+end-if
+
echo *** End Migration ***
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java
index 3963b58..d5c6b60 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java
@@ -18,6 +18,7 @@
package org.keycloak.authentication.authenticators.x509;
+import java.security.GeneralSecurityException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.function.Function;
@@ -34,6 +35,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.x509.X509ClientCertificateLookup;
/**
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
@@ -46,8 +48,6 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
public static final String DEFAULT_ATTRIBUTE_NAME = "usercertificate";
protected static ServicesLogger logger = ServicesLogger.LOGGER;
- public static final String JAVAX_SERVLET_REQUEST_X509_CERTIFICATE = "javax.servlet.request.X509Certificate";
-
public static final String REGULAR_EXPRESSION = "x509-cert-auth.regular-expression";
public static final String ENABLE_CRL = "x509-cert-auth.crl-checking-enabled";
public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-checking-enabled";
@@ -190,16 +190,29 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
}
protected X509Certificate[] getCertificateChain(AuthenticationFlowContext context) {
- // Get a x509 client certificate
- X509Certificate[] certs = (X509Certificate[]) context.getHttpRequest().getAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);
+ try {
+ // Get a x509 client certificate
+ X509ClientCertificateLookup provider = context.getSession().getProvider(X509ClientCertificateLookup.class);
+ if (provider == null) {
+ logger.errorv("\"{0}\" Spi is not available, did you forget to update the configuration?",
+ X509ClientCertificateLookup.class);
+ return null;
+ }
+
+ X509Certificate[] certs = provider.getCertificateChain(context.getHttpRequest());
- if (certs != null) {
- for (X509Certificate cert : certs) {
- logger.tracef("[X509ClientCertificateAuthenticator:getCertificateChain] \"%s\"", cert.getSubjectDN().getName());
+ if (certs != null) {
+ for (X509Certificate cert : certs) {
+ logger.tracev("\"{0}\"", cert.getSubjectDN().getName());
+ }
}
- }
- return certs;
+ return certs;
+ }
+ catch (GeneralSecurityException e) {
+ logger.error(e.getMessage(), e);
+ }
+ return null;
}
// Purely for unit testing
public UserIdentityExtractor getUserIdentityExtractor(X509AuthenticatorConfigModel config) {
diff --git a/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java b/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java
new file mode 100644
index 0000000..75b432e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookup.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.common.util.PemException;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 3/29/2017
+ */
+
+public abstract class AbstractClientCertificateFromHttpHeadersLookup implements X509ClientCertificateLookup {
+
+ protected static final Logger logger = Logger.getLogger(AbstractClientCertificateFromHttpHeadersLookup.class);
+
+ protected final String sslClientCertHttpHeader;
+ protected final String sslCertChainHttpHeaderPrefix;
+ private final int certificateChainLength;
+
+ public AbstractClientCertificateFromHttpHeadersLookup(String sslCientCertHttpHeader,
+ String sslCertChainHttpHeaderPrefix,
+ int certificateChainLength) {
+ if (sslCientCertHttpHeader == null) {
+ throw new IllegalArgumentException("sslClientCertHttpHeader");
+ }
+
+ if (certificateChainLength < 0) {
+ throw new IllegalArgumentException("certificateChainLength must be greater or equal to zero");
+ }
+
+ this.sslClientCertHttpHeader = sslCientCertHttpHeader;
+ this.sslCertChainHttpHeaderPrefix = sslCertChainHttpHeaderPrefix;
+ this.certificateChainLength = certificateChainLength;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ static String getHeaderValue(HttpRequest httpRequest, String headerName) {
+ return httpRequest.getHttpHeaders().getRequestHeaders().getFirst(headerName);
+ }
+
+ private static String trimDoubleQuotes(String quotedString) {
+
+ if (quotedString == null) return null;
+
+ int len = quotedString.length();
+ if (len > 1 && quotedString.charAt(0) == '"' &&
+ quotedString.charAt(len - 1) == '"') {
+ logger.trace("Detected a certificate enclosed in double quotes");
+ return quotedString.substring(1, len - 1);
+ }
+ return quotedString;
+ }
+
+ protected abstract X509Certificate decodeCertificateFromPem(String pem) throws PemException;
+
+ protected X509Certificate getCertificateFromHttpHeader(HttpRequest request, String httpHeader) throws GeneralSecurityException {
+
+ String encodedCertificate = getHeaderValue(request, httpHeader);
+
+ // Remove double quotes
+ encodedCertificate = trimDoubleQuotes(encodedCertificate);
+
+ if (encodedCertificate == null ||
+ encodedCertificate.trim().length() == 0) {
+ logger.warnf("HTTP header \"%s\" is empty", httpHeader);
+ return null;
+ }
+
+ try {
+ X509Certificate cert = decodeCertificateFromPem(encodedCertificate);
+ if (cert == null) {
+ logger.warnf("HTTP header \"%s\" does not contain a valid x.509 certificate\n%s",
+ httpHeader, encodedCertificate);
+ } else {
+ logger.debugf("Found a valid x.509 certificate in \"%s\" HTTP header",
+ httpHeader);
+ }
+ return cert;
+ }
+ catch(PemException e) {
+ logger.error(e.getMessage(), e);
+ throw new GeneralSecurityException(e);
+ }
+ }
+
+
+ @Override
+ public X509Certificate[] getCertificateChain(HttpRequest httpRequest) throws GeneralSecurityException {
+ List<X509Certificate> chain = new ArrayList<>();
+
+ // Get the client certificate
+ X509Certificate cert = getCertificateFromHttpHeader(httpRequest, sslClientCertHttpHeader);
+ if (cert != null) {
+ chain.add(cert);
+ // Get the certificate of the client certificate chain
+ for (int i = 0; i < certificateChainLength; i++) {
+ try {
+ String s = String.format("{0}_{1}", sslCertChainHttpHeaderPrefix, i);
+ cert = getCertificateFromHttpHeader(httpRequest, s);
+ if (cert != null) {
+ chain.add(cert);
+ }
+ }
+ catch(GeneralSecurityException e) {
+ logger.warn(e.getMessage(), e);
+ }
+ }
+ }
+ return chain.toArray(new X509Certificate[0]);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookupFactory.java b/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookupFactory.java
new file mode 100644
index 0000000..e8d24f6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/AbstractClientCertificateFromHttpHeadersLookupFactory.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 4/4/2017
+ */
+
+public abstract class AbstractClientCertificateFromHttpHeadersLookupFactory implements X509ClientCertificateLookupFactory {
+
+ private final static Logger logger = Logger.getLogger(AbstractClientCertificateFromHttpHeadersLookupFactory.class);
+
+ protected final static String CERTIFICATE_CHAIN_LENGTH = "certificateChainLength";
+ protected final static String HTTP_HEADER_CLIENT_CERT = "sslClientCert";
+ protected final static String HTTP_HEADER_CERT_CHAIN_PREFIX = "sslCertChainPrefix";
+
+ protected String sslClientCertHttpHeader;
+ protected String sslChainHttpHeaderPrefix;
+ protected int certificateChainLength = 1;
+
+ @Override
+ public void init(Config.Scope config) {
+ if (config != null) {
+ certificateChainLength = config.getInt(CERTIFICATE_CHAIN_LENGTH, 1);
+ logger.tracev("{0}: '{1}'", CERTIFICATE_CHAIN_LENGTH, certificateChainLength);
+
+ sslClientCertHttpHeader = config.get(HTTP_HEADER_CLIENT_CERT, "");
+ logger.tracev("{0}: '{1}'", HTTP_HEADER_CLIENT_CERT, sslClientCertHttpHeader);
+
+ sslChainHttpHeaderPrefix = config.get(HTTP_HEADER_CERT_CHAIN_PREFIX, null);
+ if (sslChainHttpHeaderPrefix != null) {
+ logger.tracev("{0}: '{1}'", HTTP_HEADER_CERT_CHAIN_PREFIX, sslChainHttpHeaderPrefix);
+ } else {
+ logger.tracev("{0} was not configured", HTTP_HEADER_CERT_CHAIN_PREFIX);
+ }
+ }
+ else {
+ logger.tracev("No configuration for '{0}' reverse proxy was found", this.getId());
+ sslClientCertHttpHeader = "";
+ sslChainHttpHeaderPrefix = "";
+ certificateChainLength = 0;
+ }
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookup.java b/services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookup.java
new file mode 100644
index 0000000..aa90b0d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookup.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.keycloak.common.util.PemException;
+import org.keycloak.common.util.PemUtils;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * The provider allows to extract X.509 client certificate forwarded
+ * to keycloak configured behind the Apache reverse proxy.
+ *
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 3/29/2017
+ */
+
+public class ApacheProxySslClientCertificateLookup extends AbstractClientCertificateFromHttpHeadersLookup {
+
+ public ApacheProxySslClientCertificateLookup(String sslCientCertHttpHeader,
+ String sslCertChainHttpHeaderPrefix,
+ int certificateChainLength) {
+ super(sslCientCertHttpHeader, sslCertChainHttpHeaderPrefix, certificateChainLength);
+ }
+
+ private static String removeBeginEnd(String pem) {
+ pem = pem.replace("-----BEGIN CERTIFICATE-----", "");
+ pem = pem.replace("-----END CERTIFICATE-----", "");
+ pem = pem.replace("\r\n", "");
+ pem = pem.replace("\n", "");
+ return pem.trim();
+ }
+
+ @Override
+ protected X509Certificate decodeCertificateFromPem(String pem) throws PemException {
+ if (pem == null) {
+ return null;
+ }
+ if (pem.startsWith("-----BEGIN CERTIFICATE-----")) {
+ pem = removeBeginEnd(pem);
+ }
+ return PemUtils.decodeCertificate(pem);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookupFactory.java b/services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookupFactory.java
new file mode 100644
index 0000000..c02a76d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/ApacheProxySslClientCertificateLookupFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 4/4/2017
+ */
+
+public class ApacheProxySslClientCertificateLookupFactory extends AbstractClientCertificateFromHttpHeadersLookupFactory {
+
+ private final static String PROVIDER = "apache";
+
+ @Override
+ public X509ClientCertificateLookup create(KeycloakSession session) {
+ return new ApacheProxySslClientCertificateLookup(sslClientCertHttpHeader,
+ sslChainHttpHeaderPrefix, certificateChainLength);
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/DefaultClientCertificateLookup.java b/services/src/main/java/org/keycloak/services/x509/DefaultClientCertificateLookup.java
new file mode 100644
index 0000000..e8927d9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/DefaultClientCertificateLookup.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
+import java.security.cert.X509Certificate;
+
+/**
+ * The provider retrieves a client certificate and the certificate chain
+ * (if any) from the incoming TLS connection.
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 3/26/2017
+ */
+
+public class DefaultClientCertificateLookup implements X509ClientCertificateLookup {
+
+ private static final Logger logger = Logger.getLogger(DefaultClientCertificateLookup.class);
+
+ public static final String JAVAX_SERVLET_REQUEST_X509_CERTIFICATE = "javax.servlet.request.X509Certificate";
+
+ public DefaultClientCertificateLookup() {
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(HttpRequest httpRequest) {
+
+ X509Certificate[] certs = (X509Certificate[]) httpRequest.getAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);
+ if (certs != null) {
+ for (X509Certificate cert : certs) {
+ logger.tracef("Certificate's SubjectDN => \"%s\"", cert.getSubjectDN().getName());
+ }
+ }
+ return certs;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/DefaultClientCertificateLookupFactory.java b/services/src/main/java/org/keycloak/services/x509/DefaultClientCertificateLookupFactory.java
new file mode 100644
index 0000000..20b84c7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/DefaultClientCertificateLookupFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * The factory and the corresponding providers extract a client certificate
+ * and the certificate chain (if any) from the incoming TLS connection.
+ *
+ * This factory is used by default
+ *
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 4/4/2017
+ */
+
+public class DefaultClientCertificateLookupFactory implements X509ClientCertificateLookupFactory {
+
+ private final static String PROVIDER = "default";
+ private final static X509ClientCertificateLookup SINGLETON =
+ new DefaultClientCertificateLookup();
+
+ @Override
+ public X509ClientCertificateLookup create(KeycloakSession session) {
+ return SINGLETON;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/HaProxySslClientCertificateLookup.java b/services/src/main/java/org/keycloak/services/x509/HaProxySslClientCertificateLookup.java
new file mode 100644
index 0000000..a3d270c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/HaProxySslClientCertificateLookup.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.PemException;
+import org.keycloak.common.util.PemUtils;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * The provider allows to extract X.509 client certificate forwarded
+ * to the keycloak middleware configured behind the haproxy reverse proxy.
+ *
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 3/27/2017
+ */
+
+public class HaProxySslClientCertificateLookup extends AbstractClientCertificateFromHttpHeadersLookup {
+
+ private static final Logger logger = Logger.getLogger(HaProxySslClientCertificateLookup.class);
+
+ public HaProxySslClientCertificateLookup(String sslCientCertHttpHeader,
+ String sslCertChainHttpHeaderPrefix,
+ int certificateChainLength) {
+ super(sslCientCertHttpHeader, sslCertChainHttpHeaderPrefix, certificateChainLength);
+ }
+
+ @Override
+ protected X509Certificate decodeCertificateFromPem(String pem) throws PemException {
+
+ if (pem == null) {
+ return null;
+ }
+ return PemUtils.decodeCertificate(pem);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/HaProxySslClientCertificateLookupFactory.java b/services/src/main/java/org/keycloak/services/x509/HaProxySslClientCertificateLookupFactory.java
new file mode 100644
index 0000000..12af286
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/HaProxySslClientCertificateLookupFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 4/4/2017
+ */
+
+public class HaProxySslClientCertificateLookupFactory extends AbstractClientCertificateFromHttpHeadersLookupFactory {
+
+ private final static String PROVIDER = "haproxy";
+ @Override
+ public X509ClientCertificateLookup create(KeycloakSession session) {
+ return new HaProxySslClientCertificateLookup(sslClientCertHttpHeader,
+ sslChainHttpHeaderPrefix, certificateChainLength);
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookup.java b/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookup.java
new file mode 100644
index 0000000..abeca18
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookup.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.provider.Provider;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.X509Certificate;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 3/26/2017
+ */
+
+public interface X509ClientCertificateLookup extends Provider {
+
+ /**
+ * Returns a client certificate, and optionally any certificates
+ * in the certificate chain.
+ * @return
+ */
+ X509Certificate[] getCertificateChain(HttpRequest httpRequest) throws GeneralSecurityException;
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookupFactory.java b/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookupFactory.java
new file mode 100644
index 0000000..e3a607b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookupFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 3/26/2017
+ */
+
+public interface X509ClientCertificateLookupFactory extends ProviderFactory<X509ClientCertificateLookup> {
+
+}
diff --git a/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookupSpi.java b/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookupSpi.java
new file mode 100644
index 0000000..dd10d7f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/x509/X509ClientCertificateLookupSpi.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.keycloak.services.x509;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 3/26/2017
+ */
+
+public class X509ClientCertificateLookupSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "x509cert-lookup";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return X509ClientCertificateLookup.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return X509ClientCertificateLookupFactory.class;
+ }
+}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 873ff8d..7027379 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -20,3 +20,4 @@ org.keycloak.wellknown.WellKnownSpi
org.keycloak.services.clientregistration.ClientRegistrationSpi
org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi
org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi
+org.keycloak.services.x509.X509ClientCertificateLookupSpi
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory
new file mode 100644
index 0000000..7aef09b
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.services.x509.X509ClientCertificateLookupFactory
@@ -0,0 +1,21 @@
+#
+# Copyright 2017 Analytical Graphics, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+
+org.keycloak.services.x509.DefaultClientCertificateLookupFactory
+org.keycloak.services.x509.HaProxySslClientCertificateLookupFactory
+org.keycloak.services.x509.ApacheProxySslClientCertificateLookupFactory
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 22d5ebc..3bb4d2a 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -143,6 +143,25 @@
"http=${auth.server.http.port}",
"https=${auth.server.https.port}"
]
+ }
+ },
+
+ "x509cert-lookup": {
+ "provider": "${keycloak.x509cert.lookup.provider:default}",
+ "default": {
+ "enabled": true
+ },
+ "haproxy": {
+ "enabled": true,
+ "sslClientCert": "x-ssl-client-cert",
+ "sslCertChainPrefix": "x-ssl-client-cert-chain",
+ "certificateChainLength": 1
+ },
+ "apache": {
+ "enabled": true,
+ "sslClientCert": "x-ssl-client-cert",
+ "sslCertChainPrefix": "x-ssl-client-cert-chain",
+ "certificateChainLength": 1
}
}
}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 7bab748..a86cd8b 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -90,7 +90,9 @@
<keycloak.connectionsJpa.url.crossdc>jdbc:h2:mem:test-dc-shared</keycloak.connectionsJpa.url.crossdc>
<keycloak.testsuite.logging.pattern>%d{HH:mm:ss,SSS} %-5p [%c] %m%n</keycloak.testsuite.logging.pattern>
- <adapter.test.props/>
+ <adapter.test.props>
+ -Dkeycloak.x509cert.lookup.provider=${keycloak.x509cert.lookup.provider}
+ </adapter.test.props>
<migration.import.properties/>
<kie.maven.settings/>
@@ -125,6 +127,7 @@
<client.key.passphrase>secret</client.key.passphrase>
<auth.server.ocsp.responder.enabled>false</auth.server.ocsp.responder.enabled>
+ <keycloak.x509cert.lookup.provider>default</keycloak.x509cert.lookup.provider>
</properties>
<build>
diff --git a/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties b/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties
index d7fab92..7cfa64a 100644
--- a/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties
+++ b/wildfly/server-subsystem/src/main/config/default-server-subsys-config.properties
@@ -73,4 +73,8 @@ keycloak.server.subsys.default.config=\
</properties>\
</provider>\
</spi>\
-</subsystem>\
\ No newline at end of file
+ <spi name="x509cert-lookup">\
+ <default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>\
+ <provider name="default" enabled="true"/>\
+ </spi>\
+</subsystem>\