keycloak-uncached

KC-4335: working on adding a reverse proxy support to allow

4/21/2017 2:01:18 PM

Changes

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>\