keycloak-aplcache

KEYCLOAK-4335: x509 client certificate authentication Started

7/26/2016 12:47:28 PM

Changes

Details

diff --git a/common/src/main/java/org/keycloak/common/util/CertificateUtils.java b/common/src/main/java/org/keycloak/common/util/CertificateUtils.java
index 7fc440e..6d554f0 100755
--- a/common/src/main/java/org/keycloak/common/util/CertificateUtils.java
+++ b/common/src/main/java/org/keycloak/common/util/CertificateUtils.java
@@ -40,6 +40,15 @@ import org.bouncycastle.operator.DigestCalculator;
 import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
 import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
 import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import java.net.URI;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.Calendar;
+import java.util.Date;
 
 import java.math.BigInteger;
 import java.security.KeyPair;
@@ -75,7 +84,6 @@ public class CertificateUtils {
      */
     public static X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert,
             String subject) throws Exception {
-
         try {
             X500Name subjectDN = new X500Name("CN=" + subject);
 
diff --git a/common/src/main/java/org/keycloak/common/util/CRLUtils.java b/common/src/main/java/org/keycloak/common/util/CRLUtils.java
new file mode 100644
index 0000000..2d649cd
--- /dev/null
+++ b/common/src/main/java/org/keycloak/common/util/CRLUtils.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 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.common.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.DERIA5String;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.x509.CRLDistPoint;
+import org.bouncycastle.asn1.x509.DistributionPoint;
+import org.bouncycastle.asn1.x509.DistributionPointName;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 10/31/2016
+ */
+
+public final class CRLUtils {
+
+    static {
+        BouncyIntegration.init();
+    }
+
+    private static final String CRL_DISTRIBUTION_POINTS_OID = "2.5.29.31";
+
+    /**
+     * Retrieves a list of CRL distribution points from CRLDP v3 certificate extension
+     * See <a href="www.nakov.com/blog/2009/12/01/x509-certificate-validation-in-java-build-and-verify-cchain-and-verify-clr-with-bouncy-castle/">CRL validation</a>
+     * @param cert
+     * @return
+     * @throws IOException
+     */
+    public static List<String> getCRLDistributionPoints(X509Certificate cert) throws IOException {
+        byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID);
+        if (data == null) {
+            return Collections.emptyList();
+        }
+
+        List<String> distributionPointUrls = new LinkedList<>();
+        DEROctetString octetString;
+        try (ASN1InputStream crldpExtensionInputStream = new ASN1InputStream(new ByteArrayInputStream(data))) {
+            octetString = (DEROctetString)crldpExtensionInputStream.readObject();
+        }
+        byte[] octets = octetString.getOctets();
+
+        CRLDistPoint crlDP;
+        try (ASN1InputStream crldpInputStream = new ASN1InputStream(new ByteArrayInputStream(octets))) {
+            crlDP = CRLDistPoint.getInstance(crldpInputStream.readObject());
+        }
+
+        for (DistributionPoint dp : crlDP.getDistributionPoints()) {
+            DistributionPointName dpn = dp.getDistributionPoint();
+            if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME) {
+                GeneralName[] names = GeneralNames.getInstance(dpn.getName()).getNames();
+                for (GeneralName gn : names) {
+                    if (gn.getTagNo() == GeneralName.uniformResourceIdentifier) {
+                        String url = DERIA5String.getInstance(gn.getName()).getString();
+                        distributionPointUrls.add(url);
+                    }
+                }
+            }
+        }
+
+        return distributionPointUrls;
+    }
+
+}
diff --git a/common/src/main/java/org/keycloak/common/util/OCSPUtils.java b/common/src/main/java/org/keycloak/common/util/OCSPUtils.java
new file mode 100644
index 0000000..59eaab2
--- /dev/null
+++ b/common/src/main/java/org/keycloak/common/util/OCSPUtils.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright 2016 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.common.util;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.DERIA5String;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
+import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.CRLReason;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
+import org.bouncycastle.asn1.x509.AccessDescription;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.ocsp.*;
+import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
+import org.bouncycastle.operator.ContentVerifierProvider;
+import org.bouncycastle.operator.DigestCalculator;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
+import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
+
+import java.io.*;
+import java.math.BigInteger;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.cert.*;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 10/29/2016
+ */
+
+public final class OCSPUtils {
+
+
+    static {
+        BouncyIntegration.init();
+    }
+
+    private final static Logger logger = Logger.getLogger(""+OCSPUtils.class);
+
+    private static int OCSP_CONNECT_TIMEOUT = 10000; // 10 sec
+    private static final int TIME_SKEW = 900000;
+
+    public enum RevocationStatus {
+        GOOD,
+        REVOKED,
+        UNKNOWN
+    }
+
+    public interface OCSPRevocationStatus {
+        RevocationStatus getRevocationStatus();
+        Date getRevocationTime();
+        CRLReason getRevocationReason();
+    }
+
+    /**
+     * Requests certificate revocation status using OCSP.
+     * @param cert the certificate to be checked
+     * @param issuerCertificate The issuer certificate
+     * @param responderURI an address of OCSP responder. Overrides any OCSP responder URIs stored in certificate's AIA extension
+     * @param date
+     * @param responderCert a certificate that OCSP responder uses to sign OCSP responses
+     * @return revocation status
+     */
+    public static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate, URI responderURI, X509Certificate responderCert, Date date) throws CertPathValidatorException {
+        if (cert == null)
+            throw new IllegalArgumentException("cert cannot be null");
+        if (issuerCertificate == null)
+            throw new IllegalArgumentException("issuerCertificate cannot be null");
+        if (responderURI == null)
+            throw new IllegalArgumentException("responderURI cannot be null");
+
+        return check(cert, issuerCertificate, Collections.singletonList(responderURI), responderCert, date);
+    }
+    /**
+     * Requests certificate revocation status using OCSP. The OCSP responder URI
+     * is obtained from the certificate's AIA extension.
+     * @param cert the certificate to be checked
+     * @param issuerCertificate The issuer certificate
+     * @param date
+     * @return revocation status
+     */
+    public static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate, Date date, X509Certificate responderCert) throws CertPathValidatorException {
+        List<String> responderURIs = null;
+        try {
+            responderURIs = getResponderURIs(cert);
+        } catch (CertificateEncodingException e) {
+            logger.log(Level.FINE, "CertificateEncodingException: {0}", e.getMessage());
+            throw new CertPathValidatorException(e.getMessage(), e);
+        }
+        if (responderURIs.size() == 0) {
+            logger.log(Level.INFO, "No OCSP responders in the specified certificate");
+            throw new CertPathValidatorException("No OCSP Responder URI in certificate");
+        }
+
+        List<URI> uris = new LinkedList<>();
+        for (String value : responderURIs) {
+            try {
+                URI responderURI = URI.create(value);
+                uris.add(responderURI);
+            } catch (IllegalArgumentException ex) {
+                logger.log(Level.FINE, "Malformed responder URI {0}", value);
+            }
+        }
+        return check(cert, issuerCertificate, Collections.unmodifiableList(uris), responderCert, date);
+    }
+    /**
+     * Requests certificate revocation status using OCSP. The OCSP responder URI
+     * is obtained from the certificate's AIA extension.
+     * @param cert the certificate to be checked
+     * @param issuerCertificate The issuer certificate
+     * @return revocation status
+     */
+    public static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {
+        return check(cert, issuerCertificate, null, null);
+    }
+
+    private static OCSPResp getResponse(OCSPReq ocspReq, URI responderUri) throws IOException {
+        DataOutputStream dataOut = null;
+        InputStream in = null;
+        try {
+            byte[] array = ocspReq.getEncoded();
+            URL urlt = responderUri.toURL();
+            HttpURLConnection con = (HttpURLConnection) urlt.openConnection();
+            con.setRequestMethod("POST");
+            con.setConnectTimeout(OCSP_CONNECT_TIMEOUT);
+            con.setReadTimeout(OCSP_CONNECT_TIMEOUT);
+            con.setRequestProperty("Content-type", "application/ocsp-request");
+            con.setRequestProperty("Content-length", String.valueOf(array.length));
+//        con.setRequestProperty("Accept", "application/ocsp-response");
+
+            con.setDoOutput(true);
+            con.setDoInput(true);
+            OutputStream out = con.getOutputStream();
+            dataOut = new DataOutputStream(new BufferedOutputStream(out));
+            dataOut.write(array);
+            dataOut.flush();
+
+            if (con.getResponseCode() / 100 != 2) {
+                String errorMessage = String.format("Connection error, unable to obtain certificate revocation status using OCSP responder \"%s\", code \"%d\"",
+                        responderUri.toString(), con.getResponseCode());
+                throw new IOException(errorMessage);
+            }
+            //Get Response
+            in = (InputStream) con.getInputStream();
+            int contentLen = con.getContentLength();
+            if (contentLen == -1) {
+                contentLen = Integer.MAX_VALUE;
+            }
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            int bytesRead = 0;
+            byte[] buffer = new byte[2048];
+            while ((bytesRead = in.read(buffer, 0, buffer.length)) >= 0) {
+                baos.write(buffer, 0, bytesRead);
+            }
+            baos.flush();
+            byte[] data = baos.toByteArray();
+            return new OCSPResp(data);
+        } finally {
+            if (dataOut != null) {
+                dataOut.close();
+            }
+            if (in != null) {
+                in.close();
+            }
+        }
+    }
+
+    /**
+     * Requests certificate revocation status using OCSP.
+     * @param cert the certificate to be checked
+     * @param issuerCertificate the issuer certificate
+     * @param responderURIs the OCSP responder URIs
+     * @param responderCert the OCSP responder certificate
+     * @param date if null, the current time is used.
+     * @return a revocation status
+     * @throws CertPathValidatorException
+     */
+    private static OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate, List<URI> responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException {
+        if (responderURIs == null || responderURIs.size() == 0)
+            throw new IllegalArgumentException("Need at least one responder");
+        try {
+            DigestCalculator digCalc = new BcDigestCalculatorProvider()
+                    .get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
+
+            JcaCertificateID certificateID = new JcaCertificateID(digCalc, issuerCertificate, cert.getSerialNumber());
+
+            // Create a nounce extension to protect against replay attacks
+            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+            BigInteger nounce = BigInteger.valueOf(Math.abs(random.nextInt()));
+
+            DEROctetString derString = new DEROctetString(nounce.toByteArray());
+            Extension nounceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, derString);
+            Extensions extensions = new Extensions(nounceExtension);
+
+            OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID, extensions).build();
+
+            URI responderURI = responderURIs.get(0);
+            logger.log(Level.INFO, "OCSP Responder {0}", responderURI);
+
+            try {
+                OCSPResp resp = getResponse(ocspReq, responderURI);
+                logger.log(Level.FINE, "Received a response from OCSP responder {0}, the response status is {1}", new Object[]{responderURI, resp.getStatus()});
+                switch (resp.getStatus()) {
+                    case OCSPResp.SUCCESSFUL:
+                        if (resp.getResponseObject() instanceof BasicOCSPResp) {
+                            return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, nounce, (BasicOCSPResp)resp.getResponseObject());
+                        } else {
+                            throw new CertPathValidatorException("OCSP responder returned an invalid or unknown OCSP response.");
+                        }
+
+                    case OCSPResp.INTERNAL_ERROR:
+                    case OCSPResp.TRY_LATER:
+                        throw new CertPathValidatorException("Internal error/try later. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS);
+
+                    case OCSPResp.SIG_REQUIRED:
+                        throw new CertPathValidatorException("Invalid or missing signature. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.INVALID_SIGNATURE);
+
+                    case OCSPResp.UNAUTHORIZED:
+                        throw new CertPathValidatorException("Unauthorized request. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED);
+
+                    case OCSPResp.MALFORMED_REQUEST:
+                    default:
+                        throw new CertPathValidatorException("OCSP request is malformed. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED);
+                }
+            }
+            catch(IOException e) {
+                logger.log(Level.FINE, "OCSP Responder \"{0}\" failed to return a valid OCSP response\n{1}",
+                        new Object[] {responderURI, e.getMessage()});
+                throw new CertPathValidatorException("OCSP check failed", e);
+            }
+        }
+        catch(CertificateNotYetValidException | CertificateExpiredException | OperatorCreationException | OCSPException | CertificateEncodingException | NoSuchAlgorithmException | NoSuchProviderException e) {
+            logger.log(Level.FINE, e.getMessage());
+            throw new CertPathValidatorException(e.getMessage(), e);
+        }
+    }
+
+    private static OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse)
+            throws OCSPException, NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
+        SingleResp expectedResponse = null;
+        for (SingleResp singleResponse : basicOcspResponse.getResponses()) {
+            if (compareCertIDs(certificateID, singleResponse.getCertID())) {
+                expectedResponse = singleResponse;
+                break;
+            }
+        }
+
+        if (expectedResponse != null) {
+            verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, nounce.toByteArray(), date);
+            return singleResponseToRevocationStatus(expectedResponse);
+        } else {
+            throw new CertPathValidatorException("OCSP response does not include a response for a certificate supplied in the OCSP request");
+        }
+    }
+
+    private static boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) {
+        if (idLeft == idRight)
+            return true;
+        if (idLeft == null || idRight == null)
+            return false;
+
+        return Arrays.equals(idLeft.getIssuerKeyHash(), idRight.getIssuerKeyHash()) &&
+                Arrays.equals(idLeft.getIssuerNameHash(), idRight.getIssuerNameHash()) &&
+                idLeft.getSerialNumber().equals(idRight.getSerialNumber());
+    }
+
+    private static void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
+
+        List<X509CertificateHolder> certs = new ArrayList<>(Arrays.asList(basicOcspResponse.getCerts()));
+        X509Certificate signingCert = null;
+
+        try {
+            certs.add(new JcaX509CertificateHolder(issuerCertificate));
+            if (responderCertificate != null) {
+                certs.add(new JcaX509CertificateHolder(responderCertificate));
+            }
+        } catch (CertificateEncodingException e) {
+            e.printStackTrace();
+        }
+        if (certs.size() > 0) {
+
+            X500Name responderName = basicOcspResponse.getResponderId().toASN1Object().getName();
+            byte[] responderKey = basicOcspResponse.getResponderId().toASN1Object().getKeyHash();
+
+            if (responderName != null) {
+                logger.log(Level.INFO, "Responder Name: {0}", responderName.toString());
+                for (X509CertificateHolder certHolder : certs) {
+                    try {
+                        X509Certificate tempCert = new JcaX509CertificateConverter()
+                                .setProvider("BC").getCertificate(certHolder);
+                        X500Name respName = new X500Name(tempCert.getSubjectX500Principal().getName());
+                        if (responderName.equals(respName)) {
+                            signingCert = tempCert;
+                            logger.log(Level.INFO, "Found a certificate whose principal \"{0}\" matches the responder name \"{1}\"",
+                                    new Object[] {tempCert.getSubjectDN().getName(), responderName.toString()});
+                            break;
+                        }
+                    } catch (CertificateException e) {
+                        logger.log(Level.FINE, e.getMessage());
+                    }
+                }
+            } else if (responderKey != null) {
+                SubjectKeyIdentifier responderSubjectKey = new SubjectKeyIdentifier(responderKey);
+                logger.log(Level.INFO, "Responder Key: {0}", Arrays.toString(responderKey));
+                for (X509CertificateHolder certHolder : certs) {
+                    try {
+                        X509Certificate tempCert = new JcaX509CertificateConverter()
+                                .setProvider("BC").getCertificate(certHolder);
+
+                        SubjectKeyIdentifier subjectKeyIdentifier = null;
+                        if (certHolder.getExtensions() != null) {
+                            subjectKeyIdentifier = SubjectKeyIdentifier.fromExtensions(certHolder.getExtensions());
+                        }
+
+                        if (subjectKeyIdentifier != null) {
+                            logger.log(Level.INFO, "Certificate: {0}\nSubject Key Id: {1}",
+                                    new Object[] {tempCert.getSubjectDN().getName(), Arrays.toString(subjectKeyIdentifier.getKeyIdentifier())});
+                        }
+
+                        if (subjectKeyIdentifier != null && responderSubjectKey.equals(subjectKeyIdentifier)) {
+                            signingCert = tempCert;
+                            logger.log(Level.INFO, "Found a signer certificate \"{0}\" with the subject key extension value matching the responder key",
+                                    signingCert.getSubjectDN().getName());
+
+                            break;
+                        }
+
+                        subjectKeyIdentifier = new JcaX509ExtensionUtils().createSubjectKeyIdentifier(tempCert.getPublicKey());
+                        if (responderSubjectKey.equals(subjectKeyIdentifier)) {
+                            signingCert = tempCert;
+                            logger.log(Level.INFO, "Found a certificate \"{0}\" with the subject key matching the OCSP responder key", signingCert.getSubjectDN().getName());
+                            break;
+                        }
+
+                    } catch (CertificateException e) {
+                        logger.log(Level.FINE, e.getMessage());
+                    }
+                }
+            }
+        }
+        if (signingCert != null) {
+            if (signingCert.equals(issuerCertificate)) {
+                logger.log(Level.INFO, "OCSP response is signed by the target\'s Issuing CA");
+            } else if (responderCertificate != null && signingCert.equals(responderCertificate)) {
+                // https://www.ietf.org/rfc/rfc2560.txt
+                // 2.6  OCSP Signature Authority Delegation
+                // - The responder certificate is issued to the responder by CA
+                logger.log(Level.INFO, "OCSP response is signed by an authorized responder certificate");
+            } else {
+                // 4.2.2.2  Authorized Responders
+                // 3. Includes a value of id-ad-ocspSigning in an ExtendedKeyUsage
+                // extension and is issued by the CA that issued the certificate in
+                // question."
+                if (!signingCert.getIssuerX500Principal().equals(issuerCertificate.getSubjectX500Principal())) {
+                    logger.log(Level.INFO, "Signer certificate's Issuer: {0}\nIssuer certificate's Subject: {1}",
+                            new Object[] {signingCert.getIssuerX500Principal().getName(), issuerCertificate.getSubjectX500Principal().getName()});
+                    throw new CertPathValidatorException("Responder\'s certificate is not authorized to sign OCSP responses");
+                }
+                try {
+                    List<String> purposes = signingCert.getExtendedKeyUsage();
+                    if (purposes != null && !purposes.contains(KeyPurposeId.id_kp_OCSPSigning.getId())) {
+                        logger.log(Level.INFO, "OCSPSigning extended usage is not set");
+                        throw new CertPathValidatorException("Responder\'s certificate not valid for signing OCSP responses");
+                    }
+                } catch (CertificateParsingException e) {
+                    logger.log(Level.FINE, "Failed to get certificate's extended key usage extension\n{0}", e.getMessage());
+                }
+                if (date == null) {
+                    signingCert.checkValidity();
+                } else {
+                    signingCert.checkValidity(date);
+                }
+                try {
+                    Extension noOCSPCheck = new JcaX509CertificateHolder(signingCert).getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck);
+                    // TODO If the extension is present, the OCSP client can trust the
+                    // responder's certificate for the lifetime of the certificate.
+                    logger.log(Level.INFO, "OCSP no-check extension is {0} present", noOCSPCheck == null ? "not" : "");
+                } catch (CertificateEncodingException e) {
+                    logger.log(Level.FINE, "Certificate encoding exception: {0}", e.getMessage());
+                }
+
+                try {
+                    signingCert.verify(issuerCertificate.getPublicKey());
+                    logger.log(Level.INFO, "OCSP response is signed by an Authorized Responder");
+
+                } catch (GeneralSecurityException ex) {
+                    signingCert = null;
+                }
+            }
+        }
+        if (signingCert == null) {
+            throw new CertPathValidatorException("Unable to verify OCSP Response\'s signature");
+        } else {
+            if (!verifySignature(basicOcspResponse, signingCert)) {
+                throw new CertPathValidatorException("Error verifying OCSP Response\'s signature");
+            } else {
+                Extension responseNonce = basicOcspResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
+                if (responseNonce != null && requestNonce != null && !Arrays.equals(requestNonce, responseNonce.getExtnValue().getOctets())) {
+                    throw new CertPathValidatorException("Nonces do not match.");
+                } else {
+                    // See Sun's OCSP implementation.
+                    // https://www.ietf.org/rfc/rfc2560.txt, if nextUpdate is not set,
+                    // the responder is indicating that newer update is avilable all the time
+                    long current = date == null ? System.currentTimeMillis() : date.getTime();
+                    Date stop = new Date(current + (long) TIME_SKEW);
+                    Date start = new Date(current - (long) TIME_SKEW);
+
+                    Iterator<SingleResp> iter = Arrays.asList(basicOcspResponse.getResponses()).iterator();
+                    SingleResp singleRes = null;
+                    do {
+                        if (!iter.hasNext()) {
+                            return;
+                        }
+                        singleRes = iter.next();
+                    }
+                    while (!stop.before(singleRes.getThisUpdate()) &&
+                            !start.after(singleRes.getNextUpdate() != null ? singleRes.getNextUpdate() : singleRes.getThisUpdate()));
+
+                    throw new CertPathValidatorException("Response is unreliable: its validity interval is out-of-date");
+                }
+            }
+        }
+    }
+
+    private static boolean verifySignature(BasicOCSPResp basicOcspResponse, X509Certificate cert) {
+        try {
+            ContentVerifierProvider contentVerifier = new JcaContentVerifierProviderBuilder()
+                    .setProvider("BC").build(cert.getPublicKey());
+            return basicOcspResponse.isSignatureValid(contentVerifier);
+        } catch (OperatorCreationException e) {
+            logger.log(Level.FINE, "Unable to construct OCSP content signature verifier\n{0}", e.getMessage());
+        } catch (OCSPException e) {
+            logger.log(Level.FINE, "Unable to validate OCSP response signature\n{0}", e.getMessage());
+        }
+        return false;
+    }
+
+    private static OCSPRevocationStatus unknownStatus() {
+        return new OCSPRevocationStatus() {
+            @Override
+            public RevocationStatus getRevocationStatus() {
+                return RevocationStatus.UNKNOWN;
+            }
+
+            @Override
+            public Date getRevocationTime() {
+                return new Date(System.currentTimeMillis());
+            }
+
+            @Override
+            public CRLReason getRevocationReason() {
+                return CRLReason.lookup(CRLReason.unspecified);
+            }
+        };
+    }
+
+    private static OCSPRevocationStatus singleResponseToRevocationStatus(final SingleResp singleResponse) throws CertPathValidatorException {
+        final CertificateStatus certStatus = singleResponse.getCertStatus();
+
+        int revocationReason = CRLReason.unspecified;
+        Date revocationTime = null;
+        RevocationStatus status = RevocationStatus.UNKNOWN;
+        if (certStatus == CertificateStatus.GOOD) {
+            status = RevocationStatus.GOOD;
+        } else if (certStatus instanceof RevokedStatus) {
+            RevokedStatus revoked = (RevokedStatus)certStatus;
+            revocationTime = revoked.getRevocationTime();
+            status = RevocationStatus.REVOKED;
+            if (revoked.hasRevocationReason()) {
+                revocationReason = revoked.getRevocationReason();
+            }
+        } else if (certStatus instanceof UnknownStatus) {
+            status = RevocationStatus.UNKNOWN;
+        } else {
+            throw new CertPathValidatorException("Unrecognized revocation status received from OCSP.");
+        }
+
+        final RevocationStatus finalStatus = status;
+        final Date finalRevocationTime = revocationTime;
+        final int finalRevocationReason = revocationReason;
+        return new OCSPRevocationStatus() {
+            @Override
+            public RevocationStatus getRevocationStatus() {
+                return finalStatus;
+            }
+
+            @Override
+            public Date getRevocationTime() {
+                return finalRevocationTime;
+            }
+
+            @Override
+            public CRLReason getRevocationReason() {
+                return CRLReason.lookup(finalRevocationReason);
+            }
+        };
+    }
+
+
+    /**
+     * Extracts OCSP responder URI from X509 AIA v3 extension, if available. There can be
+     * multiple responder URIs encoded in the certificate.
+     * @param cert
+     * @return a list of available responder URIs.
+     * @throws CertificateEncodingException
+     */
+    private static List<String> getResponderURIs(X509Certificate cert) throws CertificateEncodingException {
+
+        LinkedList<String> responderURIs = new LinkedList<>();
+        JcaX509CertificateHolder holder = new JcaX509CertificateHolder(cert);
+        Extension aia = holder.getExtension(Extension.authorityInfoAccess);
+        if (aia != null) {
+            try {
+                ASN1InputStream in = new ASN1InputStream(aia.getExtnValue().getOctetStream());
+                ASN1Sequence seq = (ASN1Sequence)in.readObject();
+                AuthorityInformationAccess authorityInfoAccess = AuthorityInformationAccess.getInstance(seq);
+                for (AccessDescription ad : authorityInfoAccess.getAccessDescriptions()) {
+                    if (ad.getAccessMethod().equals(AccessDescription.id_ad_ocsp)) {
+                        // See https://www.ietf.org/rfc/rfc2560.txt, 3.1 Certificate Content
+                        if (ad.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) {
+                            DERIA5String value = DERIA5String.getInstance(ad.getAccessLocation().getName());
+                            responderURIs.add(value.getString());
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return responderURIs;
+    }
+}
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
index 35288c6..968895d 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
@@ -66,5 +66,6 @@
         <module name="org.apache.httpcomponents"/>
         <module name="org.twitter4j"/>
         <module name="javax.transaction.api"/>
+        <module name="sun.jdk"/>
     </dependencies>
 </module>
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
index e445d57..fa7ed05 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java
@@ -27,6 +27,9 @@ import org.keycloak.admin.client.resource.ServerInfoResource;
 import org.keycloak.admin.client.token.TokenManager;
 
 import javax.net.ssl.SSLContext;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
 import java.net.URI;
 import java.security.KeyStore;
 
@@ -46,6 +49,7 @@ public class Keycloak {
     private String authToken;
     private final ResteasyWebTarget target;
     private final ResteasyClient client;
+    private static final boolean authServerSslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
 
     Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient, String authtoken) {
         config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
@@ -70,6 +74,20 @@ public class Keycloak {
         return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client, null);
     }
 
+    private static ResteasyClientBuilder newResteasyClientBuilder() {
+        if (authServerSslRequired) {
+            // Disable PKIX path validation errors when running tests using SSL
+            HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+                @Override
+                public boolean verify(String hostName, SSLSession session) {
+                    return true;
+                }
+            };
+            return new ResteasyClientBuilder().disableTrustManager().hostnameVerifier(hostnameVerifier);
+        }
+        return new ResteasyClientBuilder();
+    }
+
     public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
         return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null, null);
     }
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
new file mode 100644
index 0000000..3963b58
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.function.Function;
+
+import javax.ws.rs.core.Response;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.ServicesLogger;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/31/2016
+ */
+
+public abstract class AbstractX509ClientCertificateAuthenticator implements Authenticator {
+
+    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";
+    public static final String ENABLE_CRLDP = "x509-cert-auth.crldp-checking-enabled";
+    public static final String CRL_RELATIVE_PATH = "x509-cert-auth.crl-relative-path";
+    public static final String OCSPRESPONDER_URI = "x509-cert-auth.ocsp-responder-uri";
+    public static final String MAPPING_SOURCE_SELECTION = "x509-cert-auth.mapping-source-selection";
+    public static final String MAPPING_SOURCE_CERT_SUBJECTDN = "Match SubjectDN using regular expression";
+    public static final String MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL = "Subject's e-mail";
+    public static final String MAPPING_SOURCE_CERT_SUBJECTDN_CN = "Subject's Common Name";
+    public static final String MAPPING_SOURCE_CERT_ISSUERDN = "Match IssuerDN using regular expression";
+    public static final String MAPPING_SOURCE_CERT_ISSUERDN_EMAIL = "Issuer's e-mail";
+    public static final String MAPPING_SOURCE_CERT_ISSUERDN_CN = "Issuer's Common Name";
+    public static final String MAPPING_SOURCE_CERT_SERIALNUMBER = "Certificate Serial Number";
+    public static final String USER_MAPPER_SELECTION = "x509-cert-auth.mapper-selection";
+    public static final String USER_ATTRIBUTE_MAPPER = "Custom Attribute Mapper";
+    public static final String USERNAME_EMAIL_MAPPER = "Username or Email";
+    public static final String CUSTOM_ATTRIBUTE_NAME = "x509-cert-auth.mapper-selection.user-attribute-name";
+    public static final String CERTIFICATE_KEY_USAGE = "x509-cert-auth.keyusage";
+    public static final String CERTIFICATE_EXTENDED_KEY_USAGE = "x509-cert-auth.extendedkeyusage";
+    static final String DEFAULT_MATCH_ALL_EXPRESSION = "(.*?)(?:$)";
+    public static final String CONFIRMATION_PAGE_DISALLOWED = "x509-cert-auth.confirmation-page-disallowed";
+
+
+    protected Response createInfoResponse(AuthenticationFlowContext context, String infoMessage, Object ... parameters) {
+        LoginFormsProvider form = context.form();
+        return form.setInfo(infoMessage, parameters).createInfoPage();
+    }
+
+    protected static class CertificateValidatorConfigBuilder {
+
+        static CertificateValidator.CertificateValidatorBuilder fromConfig(X509AuthenticatorConfigModel config) throws Exception {
+
+            CertificateValidator.CertificateValidatorBuilder builder = new CertificateValidator.CertificateValidatorBuilder();
+            return builder
+                    .keyUsage()
+                        .parse(config.getKeyUsage())
+                    .extendedKeyUsage()
+                        .parse(config.getExtendedKeyUsage())
+                    .revocation()
+                        .cRLEnabled(config.getCRLEnabled())
+                        .cRLDPEnabled(config.getCRLDistributionPointEnabled())
+                        .cRLrelativePath(config.getCRLRelativePath())
+                        .oCSPEnabled(config.getOCSPEnabled())
+                        .oCSPResponderURI(config.getOCSPResponder());
+        }
+    }
+
+    // The method is purely for purposes of facilitating the unit testing
+    public CertificateValidator.CertificateValidatorBuilder certificateValidationParameters(X509AuthenticatorConfigModel config) throws Exception {
+        return CertificateValidatorConfigBuilder.fromConfig(config);
+    }
+
+    protected static class UserIdentityExtractorBuilder {
+
+        private static final Function<X509Certificate[],X500Name> subject = certs -> {
+            try {
+                return new JcaX509CertificateHolder(certs[0]).getSubject();
+            } catch (CertificateEncodingException e) {
+                logger.warn("Unable to get certificate Subject", e);
+            }
+            return null;
+        };
+
+        private static final Function<X509Certificate[],X500Name> issuer = certs -> {
+            try {
+                return new JcaX509CertificateHolder(certs[0]).getIssuer();
+            } catch (CertificateEncodingException e) {
+                logger.warn("Unable to get certificate Issuer", e);
+            }
+            return null;
+        };
+
+        static UserIdentityExtractor fromConfig(X509AuthenticatorConfigModel config) {
+
+            X509AuthenticatorConfigModel.MappingSourceType userIdentitySource = config.getMappingSourceType();
+            String pattern = config.getRegularExpression();
+
+            UserIdentityExtractor extractor = null;
+            switch(userIdentitySource) {
+
+                case SUBJECTDN:
+                    extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, certs -> certs[0].getSubjectDN().getName());
+                    break;
+                case ISSUERDN:
+                    extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, certs -> certs[0].getIssuerDN().getName());
+                    break;
+                case SERIALNUMBER:
+                    extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> certs[0].getSerialNumber().toString());
+                    break;
+                case SUBJECTDN_CN:
+                    extractor = UserIdentityExtractor.getX500NameExtractor(BCStyle.CN, subject);
+                    break;
+                case SUBJECTDN_EMAIL:
+                    extractor = UserIdentityExtractor
+                            .either(UserIdentityExtractor.getX500NameExtractor(BCStyle.EmailAddress, subject))
+                            .or(UserIdentityExtractor.getX500NameExtractor(BCStyle.E, subject));
+                    break;
+                case ISSUERDN_CN:
+                    extractor = UserIdentityExtractor.getX500NameExtractor(BCStyle.CN, issuer);
+                    break;
+                case ISSUERDN_EMAIL:
+                    extractor = UserIdentityExtractor
+                            .either(UserIdentityExtractor.getX500NameExtractor(BCStyle.EmailAddress, issuer))
+                            .or(UserIdentityExtractor.getX500NameExtractor(BCStyle.E, issuer));
+                    break;
+                default:
+                    logger.warnf("[UserIdentityExtractorBuilder:fromConfig] Unknown or unsupported user identity source: \"%s\"", userIdentitySource.getName());
+                    break;
+            }
+            return extractor;
+        }
+    }
+
+    protected static class UserIdentityToModelMapperBuilder {
+
+        static UserIdentityToModelMapper fromConfig(X509AuthenticatorConfigModel config) {
+
+            X509AuthenticatorConfigModel.IdentityMapperType mapperType = config.getUserIdentityMapperType();
+            String attributeName = config.getCustomAttributeName();
+
+            UserIdentityToModelMapper mapper = null;
+            switch (mapperType) {
+                case USER_ATTRIBUTE:
+                    mapper = UserIdentityToModelMapper.getUserIdentityToCustomAttributeMapper(attributeName);
+                    break;
+                case USERNAME_EMAIL:
+                    mapper = UserIdentityToModelMapper.getUsernameOrEmailMapper();
+                    break;
+                default:
+                    logger.warnf("[UserIdentityToModelMapperBuilder:fromConfig] Unknown or unsupported user identity mapper: \"%s\"", mapperType.getName());
+            }
+            return mapper;
+        }
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    protected X509Certificate[] getCertificateChain(AuthenticationFlowContext context) {
+        // Get a x509 client certificate
+        X509Certificate[] certs = (X509Certificate[]) context.getHttpRequest().getAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);
+
+        if (certs != null) {
+            for (X509Certificate cert : certs) {
+                logger.tracef("[X509ClientCertificateAuthenticator:getCertificateChain] \"%s\"", cert.getSubjectDN().getName());
+            }
+        }
+
+        return certs;
+    }
+    // Purely for unit testing
+    public UserIdentityExtractor getUserIdentityExtractor(X509AuthenticatorConfigModel config) {
+        return UserIdentityExtractorBuilder.fromConfig(config);
+    }
+    // Purely for unit testing
+    public UserIdentityToModelMapper getUserIdentityToModelMapper(X509AuthenticatorConfigModel config) {
+        return UserIdentityToModelMapperBuilder.fromConfig(config);
+    }
+    @Override
+    public boolean requiresUser() {
+        return false;
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+        return true;
+    }
+
+    @Override
+    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java
new file mode 100644
index 0000000..03601e2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticatorFactory.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.services.ServicesLogger;
+
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_EXTENDED_KEY_USAGE;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_KEY_USAGE;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CONFIRMATION_PAGE_DISALLOWED;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CRL_RELATIVE_PATH;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CUSTOM_ATTRIBUTE_NAME;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.DEFAULT_ATTRIBUTE_NAME;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.DEFAULT_MATCH_ALL_EXPRESSION;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.ENABLE_CRL;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.ENABLE_CRLDP;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.ENABLE_OCSP;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_ISSUERDN;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_ISSUERDN_CN;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_ISSUERDN_EMAIL;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SERIALNUMBER;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN_CN;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_SELECTION;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.OCSPRESPONDER_URI;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.REGULAR_EXPRESSION;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.USERNAME_EMAIL_MAPPER;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.USER_ATTRIBUTE_MAPPER;
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.USER_MAPPER_SELECTION;
+import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE;
+import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/31/2016
+ */
+
+public abstract class AbstractX509ClientCertificateAuthenticatorFactory implements AuthenticatorFactory {
+
+    protected static ServicesLogger logger = ServicesLogger.LOGGER;
+
+    private static final String[] mappingSources = {
+            MAPPING_SOURCE_CERT_SUBJECTDN,
+            MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL,
+            MAPPING_SOURCE_CERT_SUBJECTDN_CN,
+            MAPPING_SOURCE_CERT_ISSUERDN,
+            MAPPING_SOURCE_CERT_ISSUERDN_EMAIL,
+            MAPPING_SOURCE_CERT_ISSUERDN_CN,
+            MAPPING_SOURCE_CERT_SERIALNUMBER
+    };
+
+    private static final String[] userModelMappers = {
+            USER_ATTRIBUTE_MAPPER,
+            USERNAME_EMAIL_MAPPER
+    };
+
+    protected static final List<ProviderConfigProperty> configProperties;
+    static {
+        List<String> mappingSourceTypes = new LinkedList<>();
+        for (String s : mappingSources) {
+            mappingSourceTypes.add(s);
+        }
+        ProviderConfigProperty mappingMethodList = new ProviderConfigProperty();
+        mappingMethodList.setType(ProviderConfigProperty.LIST_TYPE);
+        mappingMethodList.setName(MAPPING_SOURCE_SELECTION);
+        mappingMethodList.setLabel("User Identity Source");
+        mappingMethodList.setHelpText("Choose how to extract user identity from X509 certificate or the certificate fields. For example, SubjectDN will match the custom regular expression specified below to the value of certificate's SubjectDN field.");
+        mappingMethodList.setDefaultValue(mappingSources[0]);
+        mappingMethodList.setOptions(mappingSourceTypes);
+
+        ProviderConfigProperty regExp = new ProviderConfigProperty();
+        regExp.setType(STRING_TYPE);
+        regExp.setName(REGULAR_EXPRESSION);
+        regExp.setLabel("A regular expression to extract user identity");
+        regExp.setDefaultValue(DEFAULT_MATCH_ALL_EXPRESSION);
+        regExp.setHelpText("The regular expression to extract a user identity. The expression must contain a single group. For example, 'uniqueId=(.*?)(?:,|$)' will match 'uniqueId=somebody@company.org, CN=somebody' and give somebody@company.org");
+
+        List<String> mapperTypes = new LinkedList<>();
+        for (String m : userModelMappers) {
+            mapperTypes.add(m);
+        }
+
+        ProviderConfigProperty userMapperList = new ProviderConfigProperty();
+        userMapperList.setType(ProviderConfigProperty.LIST_TYPE);
+        userMapperList.setName(USER_MAPPER_SELECTION);
+        userMapperList.setHelpText("Choose how to map extracted user identities to users");
+        userMapperList.setLabel("User mapping method");
+        userMapperList.setDefaultValue(userModelMappers[0]);
+        userMapperList.setOptions(mapperTypes);
+
+        ProviderConfigProperty attributeOrPropertyValue = new ProviderConfigProperty();
+        attributeOrPropertyValue.setType(STRING_TYPE);
+        attributeOrPropertyValue.setName(CUSTOM_ATTRIBUTE_NAME);
+        attributeOrPropertyValue.setDefaultValue(DEFAULT_ATTRIBUTE_NAME);
+        attributeOrPropertyValue.setLabel("A name of user attribute");
+        attributeOrPropertyValue.setHelpText("A name of user attribute to map the extracted user identity to existing user. The name must be a valid, existing user attribute if User Mapping Method is set to Custom Attribute Mapper.");
+
+        ProviderConfigProperty crlCheckingEnabled = new ProviderConfigProperty();
+        crlCheckingEnabled.setType(BOOLEAN_TYPE);
+        crlCheckingEnabled.setName(ENABLE_CRL);
+        crlCheckingEnabled.setHelpText("Enable Certificate Revocation Checking using CRL");
+        crlCheckingEnabled.setLabel("CRL Checking Enabled");
+
+        ProviderConfigProperty crlDPEnabled = new ProviderConfigProperty();
+        crlDPEnabled.setType(BOOLEAN_TYPE);
+        crlDPEnabled.setName(ENABLE_CRLDP);
+        crlDPEnabled.setDefaultValue(false);
+        crlDPEnabled.setLabel("Enable CRL Distribution Point to check certificate revocation status");
+        crlDPEnabled.setHelpText("CRL Distribution Point is a starting point for CRL. CDP is optional, but most PKI authorities include CDP in their certificates.");
+
+        ProviderConfigProperty cRLRelativePath = new ProviderConfigProperty();
+        cRLRelativePath.setType(STRING_TYPE);
+        cRLRelativePath.setName(CRL_RELATIVE_PATH);
+        cRLRelativePath.setDefaultValue("crl.pem");
+        cRLRelativePath.setLabel("CRL File path");
+        cRLRelativePath.setHelpText("The path to a CRL file that contains a list of revoked certificates. Paths are assumed to be relative to $jboss.server.config.dir");
+
+        ProviderConfigProperty oCspCheckingEnabled = new ProviderConfigProperty();
+        oCspCheckingEnabled.setType(BOOLEAN_TYPE);
+        oCspCheckingEnabled.setName(ENABLE_OCSP);
+        oCspCheckingEnabled.setHelpText("Enable Certificate Revocation Checking using OCSP");
+        oCspCheckingEnabled.setLabel("OCSP Checking Enabled");
+
+        ProviderConfigProperty ocspResponderUri = new ProviderConfigProperty();
+        ocspResponderUri.setType(STRING_TYPE);
+        ocspResponderUri.setName(OCSPRESPONDER_URI);
+        ocspResponderUri.setLabel("OCSP Responder Uri");
+        ocspResponderUri.setHelpText("Clients use OCSP Responder Uri to check certificate revocation status.");
+
+        ProviderConfigProperty keyUsage = new ProviderConfigProperty();
+        keyUsage.setType(STRING_TYPE);
+        keyUsage.setName(CERTIFICATE_KEY_USAGE);
+        keyUsage.setLabel("Validate Key Usage");
+        keyUsage.setHelpText("Validates that the purpose of the key contained in the certificate (encipherment, signature, etc.) matches its intended purpose. Leaving the field blank will disable Key Usage validation. For example, 'digitalSignature, keyEncipherment' will check if the digitalSignature and keyEncipherment bits (bit 0 and bit 2 respectively) are set in certificate's X509 Key Usage extension. See RFC 5280 for a detailed definition of X509 Key Usage extension.");
+
+        ProviderConfigProperty extendedKeyUsage = new ProviderConfigProperty();
+        extendedKeyUsage.setType(STRING_TYPE);
+        extendedKeyUsage.setName(CERTIFICATE_EXTENDED_KEY_USAGE);
+        extendedKeyUsage.setLabel("Validate Extended Key Usage");
+        extendedKeyUsage.setHelpText("Validates the extended purposes of the certificate's key using certificate's Extended Key Usage extension. Leaving the field blank will disable Extended Key Usage validation. See RFC 5280 for a detailed definition of X509 Extended Key Usage extension.");
+
+        ProviderConfigProperty identityConfirmationPageDisallowed = new ProviderConfigProperty();
+        identityConfirmationPageDisallowed.setType(BOOLEAN_TYPE);
+        identityConfirmationPageDisallowed.setName(CONFIRMATION_PAGE_DISALLOWED);
+        identityConfirmationPageDisallowed.setLabel("Bypass identity confirmation");
+        identityConfirmationPageDisallowed.setHelpText("By default, the users are prompted to confirm their identity extracted from X509 client certificate. The identity confirmation prompt is skipped if the option is switched on.");
+
+        configProperties = asList(mappingMethodList,
+                regExp,
+                userMapperList,
+                attributeOrPropertyValue,
+                crlCheckingEnabled,
+                crlDPEnabled,
+                cRLRelativePath,
+                oCspCheckingEnabled,
+                ocspResponderUri,
+                keyUsage,
+                extendedKeyUsage,
+                identityConfirmationPageDisallowed);
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getReferenceCategory() {
+        return null;
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return true;
+    }
+
+    @Override
+    public boolean isUserSetupAllowed() {
+        return false;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateDirectGrantAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateDirectGrantAuthenticator.java
new file mode 100644
index 0000000..15de72c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateDirectGrantAuthenticator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationFlowContext;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/31/2016
+ */
+
+public abstract class AbstractX509ClientCertificateDirectGrantAuthenticator extends AbstractX509ClientCertificateAuthenticator {
+
+    public Response errorResponse(int status, String error, String errorDescription) {
+        Map<String, String> e = new HashMap<String, String>();
+        e.put(OAuth2Constants.ERROR, error);
+        if (errorDescription != null) {
+            e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
+        }
+        return Response.status(status).entity(e).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java
new file mode 100644
index 0000000..cf5ceaf
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import org.keycloak.common.util.CRLUtils;
+import org.keycloak.common.util.OCSPUtils;
+import org.keycloak.services.ServicesLogger;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLConnection;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509CRL;
+import java.security.cert.X509Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CRLException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+import java.util.LinkedList;
+import java.util.ArrayList;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/30/2016
+ */
+
+public class CertificateValidator {
+
+    private static final ServicesLogger logger = ServicesLogger.LOGGER;
+
+    enum KeyUsageBits {
+        DIGITAL_SIGNATURE(0, "digitalSignature"),
+        NON_REPUDIATION(1, "nonRepudiation"),
+        KEY_ENCIPHERMENT(2, "keyEncipherment"),
+        DATA_ENCIPHERMENT(3, "dataEncipherment"),
+        KEY_AGREEMENT(4, "keyAgreement"),
+        KEYCERTSIGN(5, "keyCertSign"),
+        CRLSIGN(6, "cRLSign"),
+        ENCIPHERMENT_ONLY(7, "encipherOnly"),
+        DECIPHER_ONLY(8, "decipherOnly");
+
+        private int value;
+        private String name;
+
+        KeyUsageBits(int value, String name) {
+
+            if (value < 0 || value > 8)
+                throw new IllegalArgumentException("value");
+            if (name == null || name.trim().length() == 0)
+                throw new IllegalArgumentException("name");
+            this.value = value;
+            this.name = name.trim();
+        }
+
+        public int getInt() { return this.value; }
+        public String getName() {  return this.name; }
+
+        static public KeyUsageBits parse(String name) throws IllegalArgumentException, IndexOutOfBoundsException {
+            if (name == null || name.trim().length() == 0)
+                throw new IllegalArgumentException("name");
+
+            for (KeyUsageBits bit : KeyUsageBits.values()) {
+                if (bit.getName().equalsIgnoreCase(name))
+                    return bit;
+            }
+            throw new IndexOutOfBoundsException("name");
+        }
+
+        static public KeyUsageBits fromValue(int value) throws IndexOutOfBoundsException {
+            if (value < 0 || value > 8)
+                throw new IndexOutOfBoundsException("value");
+            for (KeyUsageBits bit : KeyUsageBits.values())
+                if (bit.getInt() == value)
+                    return bit;
+            throw new IndexOutOfBoundsException("value");
+        }
+    }
+
+    public static class LdapContext {
+        private final String ldapFactoryClassName;
+
+        public LdapContext() {
+            ldapFactoryClassName = "com.sun.jndi.ldap.LdapCtxFactory";
+        }
+
+        public LdapContext(String ldapFactoryClassName) {
+            this.ldapFactoryClassName = ldapFactoryClassName;
+        }
+
+        public String getLdapFactoryClassName() {
+            return ldapFactoryClassName;
+        }
+    }
+
+    public static abstract class OCSPChecker {
+        /**
+         * Requests certificate revocation status using OCSP. The OCSP responder URI
+         * is obtained from the certificate's AIA extension.
+         * @param cert the certificate to be checked
+         * @param issuerCertificate The issuer certificate
+         * @return revocation status
+         */
+        public abstract OCSPUtils.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException;
+    }
+
+    public static abstract class CRLLoaderImpl {
+        /**
+         * Returns a collection of {@link X509CRL}
+         * @return
+         * @throws GeneralSecurityException
+         */
+        public abstract Collection<X509CRL> getX509CRLs() throws GeneralSecurityException;
+    }
+
+    public static class BouncyCastleOCSPChecker extends OCSPChecker {
+
+        private final String responderUri;
+        BouncyCastleOCSPChecker(String responderUri) {
+            this.responderUri = responderUri;
+        }
+
+        @Override
+        public OCSPUtils.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {
+
+            OCSPUtils.OCSPRevocationStatus ocspRevocationStatus = null;
+            if (responderUri == null || responderUri.trim().length() == 0) {
+                // Obtains revocation status of a certificate using OCSP and assuming
+                // most common defaults. If responderUri is not specified,
+                // then OCS responder URI is retrieved from the
+                // certificate's AIA extension.
+                // OCSP responses must be signed with the issuer certificate
+                // or with another certificate that must be:
+                // 1) signed by the issuer certificate,
+                // 2) Includes the value of OCSPsigning in ExtendedKeyUsage v3 extension
+                // 3) Certificate is valid at the time
+                ocspRevocationStatus = OCSPUtils.check(cert, issuerCertificate);
+            }
+            else {
+                URI uri;
+                try {
+                    uri = new URI(responderUri);
+                } catch (URISyntaxException e) {
+                    String message = String.format("Unable to check certificate revocation status using OCSP.\n%s", e.getMessage());
+                    throw new CertPathValidatorException(message, e);
+                }
+                logger.tracef("Responder URI \"%s\" will be used to verify revocation status of the certificate using OCSP", uri.toString());
+                // Obtains the revocation status of a certificate using OCSP.
+                // OCSP responder's certificate is assumed to be the issuer's certificate
+                // certificate.
+                // responderUri overrides the contents (if any) of the certificate's AIA extension
+                ocspRevocationStatus = OCSPUtils.check(cert, issuerCertificate, uri, null, null);
+            }
+            return ocspRevocationStatus;
+        }
+    }
+
+    public static class CRLLoaderProxy extends CRLLoaderImpl {
+        private final X509CRL _crl;
+        public CRLLoaderProxy(X509CRL crl) {
+            _crl = crl;
+        }
+        public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
+            return Collections.singleton(_crl);
+        }
+    }
+
+    public static class CRLFileLoader extends CRLLoaderImpl {
+
+        private final String cRLPath;
+        private final LdapContext ldapContext;
+
+        public CRLFileLoader(String cRLPath) {
+            this.cRLPath = cRLPath;
+            ldapContext = new LdapContext();
+        }
+
+        public CRLFileLoader(String cRLPath, LdapContext ldapContext) {
+            this.cRLPath = cRLPath;
+            this.ldapContext = ldapContext;
+
+            if (ldapContext == null)
+                throw new NullPointerException("Context cannot be null");
+        }
+        public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            Collection<X509CRL> crlColl = null;
+
+            if (cRLPath != null) {
+                if (cRLPath.startsWith("http") || cRLPath.startsWith("https")) {
+                    // load CRL using remote URI
+                    try {
+                        crlColl = loadFromURI(cf, new URI(cRLPath));
+                    } catch (URISyntaxException e) {
+                        logger.error(e.getMessage());
+                    }
+                } else if (cRLPath.startsWith("ldap")) {
+                    // load CRL from LDAP
+                    try {
+                        crlColl = loadCRLFromLDAP(cf, new URI(cRLPath));
+                    } catch(URISyntaxException e) {
+                        logger.error(e.getMessage());
+                    }
+                } else {
+                    // load CRL from file
+                    crlColl = loadCRLFromFile(cf, cRLPath);
+                }
+            }
+            if (crlColl == null || crlColl.size() == 0) {
+                String message = String.format("Unable to load CRL from \"%s\"", cRLPath);
+                throw new GeneralSecurityException(message);
+            }
+            return crlColl;
+        }
+
+        private Collection<X509CRL> loadFromURI(CertificateFactory cf, URI remoteURI) throws GeneralSecurityException {
+            try {
+                logger.debugf("Loading CRL from %s", remoteURI.toString());
+
+                URLConnection conn = remoteURI.toURL().openConnection();
+                conn.setDoInput(true);
+                conn.setUseCaches(false);
+                X509CRL crl = loadFromStream(cf, conn.getInputStream());
+                return Collections.singleton(crl);
+            }
+            catch(IOException ex) {
+                logger.errorf(ex.getMessage());
+            }
+            return Collections.emptyList();
+
+        }
+
+        private Collection<X509CRL> loadCRLFromLDAP(CertificateFactory cf, URI remoteURI) throws GeneralSecurityException {
+            Hashtable<String, String> env = new Hashtable<>(2);
+            env.put(Context.INITIAL_CONTEXT_FACTORY, ldapContext.getLdapFactoryClassName());
+            env.put(Context.PROVIDER_URL, remoteURI.toString());
+
+            try {
+                DirContext ctx = new InitialDirContext(env);
+                try {
+                    Attributes attrs = ctx.getAttributes("");
+                    Attribute cRLAttribute = attrs.get("certificateRevocationList;binary");
+                    byte[] data = (byte[])cRLAttribute.get();
+                    if (data == null || data.length == 0) {
+                        throw new CertificateException(String.format("Failed to download CRL from \"%s\"", remoteURI.toString()));
+                    }
+                    X509CRL crl = loadFromStream(cf, new ByteArrayInputStream(data));
+                    return Collections.singleton(crl);
+                } finally {
+                    ctx.close();
+                }
+            } catch (NamingException e) {
+                logger.error(e.getMessage());
+            } catch(IOException e) {
+                logger.error(e.getMessage());
+            }
+
+            return Collections.emptyList();
+        }
+
+        private Collection<X509CRL> loadCRLFromFile(CertificateFactory cf, String relativePath) throws GeneralSecurityException {
+            try {
+                String configDir = System.getProperty("jboss.server.config.dir");
+                if (configDir != null) {
+                    File f = new File(configDir + File.separator + relativePath);
+                    if (f.isFile()) {
+                        logger.debugf("Loading CRL from %s", f.getAbsolutePath());
+
+                        if (!f.canRead()) {
+                            throw new IOException(String.format("Unable to read CRL from \"%path\"", f.getAbsolutePath()));
+                        }
+                        X509CRL crl = loadFromStream(cf, new FileInputStream(f.getAbsolutePath()));
+                        return Collections.singleton(crl);
+                    }
+                }
+            }
+            catch(IOException ex) {
+                logger.errorf(ex.getMessage());
+            }
+            return Collections.emptyList();
+        }
+        private X509CRL loadFromStream(CertificateFactory cf, InputStream is) throws IOException, CRLException {
+            DataInputStream dis = new DataInputStream(is);
+            X509CRL crl = (X509CRL)cf.generateCRL(dis);
+            dis.close();
+            return crl;
+        }
+    }
+
+
+    X509Certificate[] _certChain;
+    int _keyUsageBits;
+    List<String> _extendedKeyUsage;
+    boolean _crlCheckingEnabled;
+    boolean _crldpEnabled;
+    CRLLoaderImpl _crlLoader;
+    boolean _ocspEnabled;
+    OCSPChecker ocspChecker;
+
+    public CertificateValidator() {
+
+    }
+    protected CertificateValidator(X509Certificate[] certChain,
+                         int keyUsageBits, List<String> extendedKeyUsage,
+                                   boolean cRLCheckingEnabled,
+                                   boolean cRLDPCheckingEnabled,
+                                   CRLLoaderImpl crlLoader,
+                                   boolean oCSPCheckingEnabled,
+                                   OCSPChecker ocspChecker) {
+        _certChain = certChain;
+        _keyUsageBits = keyUsageBits;
+        _extendedKeyUsage = extendedKeyUsage;
+        _crlCheckingEnabled = cRLCheckingEnabled;
+        _crldpEnabled = cRLDPCheckingEnabled;
+        _crlLoader = crlLoader;
+        _ocspEnabled = oCSPCheckingEnabled;
+        this.ocspChecker = ocspChecker;
+
+        if (ocspChecker == null)
+            throw new IllegalArgumentException("ocspChecker");
+    }
+
+    private static void validateKeyUsage(X509Certificate[] certs, int expected) throws GeneralSecurityException {
+        boolean[] keyUsageBits = certs[0].getKeyUsage();
+        if (keyUsageBits == null) {
+            if (expected != 0) {
+                String message = "Key usage extension is expected, but unavailable.";
+                throw new GeneralSecurityException(message);
+            }
+            return;
+        }
+
+        boolean isCritical = false;
+        Set critSet = certs[0].getCriticalExtensionOIDs();
+        if (critSet != null) {
+            isCritical = critSet.contains("2.5.29.15");
+        }
+
+        int n = expected;
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < keyUsageBits.length; i++, n >>= 1) {
+            boolean value = keyUsageBits[i];
+            if ((n & 1) == 1 && !value) {
+                String message = String.format("Key Usage bit \'%s\' is not set.", CertificateValidator.KeyUsageBits.fromValue(i).getName());
+                if (sb.length() > 0) sb.append("\n");
+                sb.append(message);
+
+                logger.warn(message);
+            }
+        }
+        if (sb.length() > 0) {
+            if (isCritical) {
+                throw new GeneralSecurityException(sb.toString());
+            }
+        }
+    }
+
+    private static void validateExtendedKeyUsage(X509Certificate[] certs, List<String> expectedEKU) throws GeneralSecurityException {
+        if (expectedEKU == null || expectedEKU.size() == 0) {
+            logger.debug("Extended Key Usage validation is not enabled.");
+            return;
+        }
+        List<String> extendedKeyUsage = certs[0].getExtendedKeyUsage();
+        if (extendedKeyUsage == null) {
+            String message = "Extended key usage extension is expected, but unavailable";
+            throw new GeneralSecurityException(message);
+        }
+
+        boolean isCritical = false;
+        Set critSet = certs[0].getCriticalExtensionOIDs();
+        if (critSet != null) {
+            isCritical = critSet.contains("2.5.29.37");
+        }
+
+        List<String> ekuList = new LinkedList<>();
+        extendedKeyUsage.forEach(s -> ekuList.add(s.toLowerCase()));
+
+        for (String eku : expectedEKU) {
+            if (!ekuList.contains(eku.toLowerCase())) {
+                String message = String.format("Extended Key Usage \'%s\' is missing.", eku);
+                if (isCritical) {
+                    throw new GeneralSecurityException(message);
+                }
+                logger.warn(message);
+            }
+        }
+    }
+
+    public CertificateValidator validateKeyUsage() throws GeneralSecurityException {
+        validateKeyUsage(_certChain, _keyUsageBits);
+        return this;
+    }
+    public CertificateValidator validateExtendedKeyUsage() throws GeneralSecurityException {
+        validateExtendedKeyUsage(_certChain, _extendedKeyUsage);
+        return this;
+    }
+    private void checkRevocationUsingOCSP(X509Certificate[] certs) throws GeneralSecurityException {
+
+        if (certs.length < 2) {
+            // OCSP requires a responder certificate to verify OCSP
+            // signed response.
+            String message = "OCSP requires a responder certificate. OCSP cannot be used to verify the revocation status of self-signed certificates.";
+            throw new GeneralSecurityException(message);
+        }
+
+        for (X509Certificate cert : certs) {
+            logger.debugf("Certificate: %s", cert.getSubjectDN().getName());
+        }
+
+        OCSPUtils.OCSPRevocationStatus rs = ocspChecker.check(certs[0], certs[1]);
+
+        if (rs == null) {
+            throw new GeneralSecurityException("Unable to check client revocation status using OCSP");
+        }
+
+        if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.UNKNOWN) {
+            throw new GeneralSecurityException("Unable to determine certificate's revocation status.");
+        }
+        else if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.REVOKED) {
+
+            StringBuilder sb = new StringBuilder();
+            sb.append("Certificate's been revoked.");
+            sb.append("\n");
+            sb.append(rs.getRevocationReason().toString());
+            sb.append("\n");
+            sb.append(String.format("Revoked on: %s",rs.getRevocationTime().toString()));
+
+            throw new GeneralSecurityException(sb.toString());
+        }
+    }
+
+    private static void checkRevocationStatusUsingCRL(X509Certificate[] certs, CRLLoaderImpl crLoader) throws GeneralSecurityException {
+        Collection<X509CRL> crlColl = crLoader.getX509CRLs();
+        if (crlColl != null && crlColl.size() > 0) {
+            for (X509CRL it : crlColl) {
+                if (it.isRevoked(certs[0])) {
+                    String message = String.format("Certificate has been revoked, certificate's subject: %s", certs[0].getSubjectDN().getName());
+                    logger.debug(message);
+                    throw new GeneralSecurityException(message);
+                }
+            }
+        }
+    }
+    private static List<String> getCRLDistributionPoints(X509Certificate cert) {
+        try {
+            return CRLUtils.getCRLDistributionPoints(cert);
+        }
+        catch(IOException e) {
+            logger.error(e.getMessage());
+        }
+        return new ArrayList<>();
+    }
+
+    private static void checkRevocationStatusUsingCRLDistributionPoints(X509Certificate[] certs) throws GeneralSecurityException {
+
+        List<String> distributionPoints = getCRLDistributionPoints(certs[0]);
+        if (distributionPoints == null || distributionPoints.size() == 0) {
+            throw new GeneralSecurityException("Could not find any CRL distribution points in the certificate, unable to check the certificate revocation status using CRL/DP.");
+        }
+        for (String dp : distributionPoints) {
+            logger.tracef("CRL Distribution point: \"%s\"", dp);
+            checkRevocationStatusUsingCRL(certs, new CRLFileLoader(dp));
+        }
+    }
+
+    public CertificateValidator checkRevocationStatus() throws GeneralSecurityException {
+        if (!(_crlCheckingEnabled || _ocspEnabled)) {
+            return this;
+        }
+        if (_crlCheckingEnabled) {
+            if (!_crldpEnabled) {
+                checkRevocationStatusUsingCRL(_certChain, _crlLoader /*"crl.pem"*/);
+            } else {
+                checkRevocationStatusUsingCRLDistributionPoints(_certChain);
+            }
+        }
+        if (_ocspEnabled) {
+            checkRevocationUsingOCSP(_certChain);
+        }
+        return this;
+    }
+
+    /**
+     * Configure Certificate validation
+     */
+    public static class CertificateValidatorBuilder {
+        // A hand written DSL that walks through successive steps to configure
+        // instances of CertificateValidator type. The design is an adaption of
+        // the approach described in http://programmers.stackexchange.com/questions/252067/learning-to-write-dsls-utilities-for-unit-tests-and-am-worried-about-extensablit
+
+        int _keyUsageBits;
+        List<String> _extendedKeyUsage;
+        boolean _crlCheckingEnabled;
+        boolean _crldpEnabled;
+        CRLLoaderImpl _crlLoader;
+        boolean _ocspEnabled;
+        String _responderUri;
+
+        public CertificateValidatorBuilder() {
+            _extendedKeyUsage = new LinkedList<>();
+            _keyUsageBits = 0;
+        }
+
+        public class KeyUsageValidationBuilder {
+
+            CertificateValidatorBuilder _parent;
+            KeyUsageValidationBuilder(CertificateValidatorBuilder parent) {
+                _parent = parent;
+            }
+
+            public KeyUsageValidationBuilder enableDigitalSignatureBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.DIGITAL_SIGNATURE.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enablecRLSignBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.CRLSIGN.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enableDataEncriphermentBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.DATA_ENCIPHERMENT.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enableDecipherOnlyBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.DECIPHER_ONLY.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enableEnciphermentOnlyBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.ENCIPHERMENT_ONLY.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enableKeyAgreementBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.KEY_AGREEMENT.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enableKeyEnciphermentBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.KEY_ENCIPHERMENT.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enableKeyCertSign() {
+                _keyUsageBits |= 1 << KeyUsageBits.KEYCERTSIGN.getInt();
+                return this;
+            }
+            public KeyUsageValidationBuilder enableNonRepudiationBit() {
+                _keyUsageBits |= 1 << KeyUsageBits.NON_REPUDIATION.getInt();
+                return this;
+            }
+
+            public CertificateValidatorBuilder back() {
+                return _parent;
+            }
+
+            CertificateValidatorBuilder parse(String keyUsage) {
+                if (keyUsage == null || keyUsage.trim().length() == 0)
+                    return _parent;
+
+                String[] strs = keyUsage.split("[,]");
+
+                for (String s : strs) {
+                    try {
+                        KeyUsageBits bit = KeyUsageBits.parse(s.trim());
+                        switch(bit) {
+                            case CRLSIGN: enablecRLSignBit(); break;
+                            case DATA_ENCIPHERMENT: enableDataEncriphermentBit(); break;
+                            case DECIPHER_ONLY: enableDecipherOnlyBit(); break;
+                            case DIGITAL_SIGNATURE: enableDigitalSignatureBit(); break;
+                            case ENCIPHERMENT_ONLY: enableEnciphermentOnlyBit(); break;
+                            case KEY_AGREEMENT: enableKeyAgreementBit(); break;
+                            case KEY_ENCIPHERMENT: enableKeyEnciphermentBit(); break;
+                            case KEYCERTSIGN: enableKeyCertSign(); break;
+                            case NON_REPUDIATION: enableNonRepudiationBit(); break;
+                        }
+                    }
+                    catch(IllegalArgumentException e) {
+                        logger.warnf("Unable to parse key usage bit: \"%s\"", s);
+                    }
+                    catch(IndexOutOfBoundsException e) {
+                        logger.warnf("Invalid key usage bit: \"%s\"", s);
+                    }
+                }
+
+                return _parent;
+            }
+        }
+
+        public class ExtendedKeyUsageValidationBuilder {
+
+            CertificateValidatorBuilder _parent;
+            protected ExtendedKeyUsageValidationBuilder(CertificateValidatorBuilder parent) {
+                _parent = parent;
+            }
+
+            public CertificateValidatorBuilder parse(String extendedKeyUsage) {
+                if (extendedKeyUsage == null || extendedKeyUsage.trim().length() == 0)
+                    return _parent;
+
+                String[] strs = extendedKeyUsage.split("[,;:]]");
+                for (String str : strs) {
+                    _extendedKeyUsage.add(str.trim());
+                }
+                return _parent;
+            }
+        }
+
+        public class RevocationStatusCheckBuilder {
+
+            CertificateValidatorBuilder _parent;
+            protected RevocationStatusCheckBuilder(CertificateValidatorBuilder parent) {
+                _parent = parent;
+            }
+
+            public GotCRL cRLEnabled(boolean value) {
+                _crlCheckingEnabled = value;
+                return new GotCRL();
+            }
+
+            public class GotCRL {
+                public GotCRLDP cRLDPEnabled(boolean value) {
+                    _crldpEnabled = value;
+                    return new GotCRLDP();
+                }
+            }
+
+            public class GotCRLRelativePath {
+                public GotOCSP oCSPEnabled(boolean value) {
+                    _ocspEnabled = value;
+                    return new GotOCSP();
+                }
+            }
+            public class GotCRLDP {
+                public GotCRLRelativePath cRLrelativePath(String value) {
+                    if (value != null)
+                        _crlLoader = new CRLFileLoader(value);
+                    return new GotCRLRelativePath();
+                }
+
+                public GotCRLRelativePath cRLLoader(CRLLoaderImpl cRLLoader) {
+                    if (cRLLoader != null)
+                        _crlLoader = cRLLoader;
+                    return new GotCRLRelativePath();
+                }
+            }
+
+            public class GotOCSP {
+                public CertificateValidatorBuilder oCSPResponderURI(String responderURI) {
+                    _responderUri = responderURI;
+                    return _parent;
+                }
+            }
+        }
+
+        public KeyUsageValidationBuilder keyUsage() {
+            return new KeyUsageValidationBuilder(this);
+        }
+
+        public ExtendedKeyUsageValidationBuilder extendedKeyUsage() {
+            return new ExtendedKeyUsageValidationBuilder(this);
+        }
+
+        public RevocationStatusCheckBuilder revocation() {
+            return new RevocationStatusCheckBuilder(this);
+        }
+
+        public CertificateValidator build(X509Certificate[] certs) {
+            if (_crlLoader == null) {
+                 _crlLoader = new CRLFileLoader("");
+            }
+            return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage,
+                    _crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled, new BouncyCastleOCSPChecker(_responderUri));
+        }
+    }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityExtractor.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityExtractor.java
new file mode 100644
index 0000000..ef29aec
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityExtractor.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import freemarker.template.utility.NullArgumentException;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.keycloak.services.ServicesLogger;
+
+import java.security.cert.X509Certificate;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/30/2016
+ */
+
+public abstract class UserIdentityExtractor {
+
+    private static final ServicesLogger logger = ServicesLogger.LOGGER;
+
+    public abstract Object extractUserIdentity(X509Certificate[] certs);
+
+    static class OrExtractor extends UserIdentityExtractor {
+
+        UserIdentityExtractor extractor;
+        UserIdentityExtractor other;
+        OrExtractor(UserIdentityExtractor extractor, UserIdentityExtractor other) {
+            this.extractor = extractor;
+            this.other = other;
+
+            if (this.extractor == null)
+                throw new NullArgumentException("extractor");
+            if (this.other == null)
+                throw new NullArgumentException("other");
+        }
+
+        @Override
+        public Object extractUserIdentity(X509Certificate[] certs) {
+            Object result = this.extractor.extractUserIdentity(certs);
+            if (result == null)
+                result = this.other.extractUserIdentity(certs);
+            return result;
+        }
+    }
+
+    static class X500NameRDNExtractor extends UserIdentityExtractor {
+
+        private ASN1ObjectIdentifier x500NameStyle;
+        Function<X509Certificate[],X500Name> x500Name;
+        X500NameRDNExtractor(ASN1ObjectIdentifier x500NameStyle, Function<X509Certificate[],X500Name> x500Name) {
+            this.x500NameStyle = x500NameStyle;
+            this.x500Name = x500Name;
+        }
+
+        @Override
+        public Object extractUserIdentity(X509Certificate[] certs) {
+
+            if (certs == null || certs.length == 0)
+                throw new IllegalArgumentException();
+
+            X500Name name = x500Name.apply(certs);
+            if (name != null) {
+                RDN[] rnds = name.getRDNs(x500NameStyle);
+                if (rnds != null && rnds.length > 0) {
+                    RDN cn = rnds[0];
+                    return IETFUtils.valueToString(cn.getFirst().getValue());
+                }
+            }
+            return null;
+        }
+    }
+
+    static class PatternMatcher extends UserIdentityExtractor {
+        private final String _pattern;
+        private final Function<X509Certificate[],String> _f;
+        PatternMatcher(String pattern, Function<X509Certificate[],String> valueToMatch) {
+            _pattern = pattern;
+            _f = valueToMatch;
+        }
+
+        @Override
+        public Object extractUserIdentity(X509Certificate[] certs) {
+            String value = _f.apply(certs);
+
+            Pattern r = Pattern.compile(_pattern, Pattern.CASE_INSENSITIVE);
+
+            Matcher m = r.matcher(value);
+
+            if (!m.find()) {
+                logger.debugf("[PatternMatcher:extract] No matches were found for input \"%s\", pattern=\"%s\"", value, _pattern);
+                return null;
+            }
+
+            if (m.groupCount() != 1) {
+                logger.debugf("[PatternMatcher:extract] Match produced more than a single group for input \"%s\", pattern=\"%s\"", value, _pattern);
+                return null;
+            }
+
+            return m.group(1);
+        }
+    }
+
+    static class OrBuilder {
+        UserIdentityExtractor extractor;
+        UserIdentityExtractor other;
+        OrBuilder(UserIdentityExtractor extractor) {
+            this.extractor = extractor;
+        }
+
+        public UserIdentityExtractor or(UserIdentityExtractor other) {
+            return new OrExtractor(extractor, other);
+        }
+    }
+
+    public static UserIdentityExtractor getPatternIdentityExtractor(String pattern,
+                                                                 Function<X509Certificate[],String> func) {
+        return new PatternMatcher(pattern, func);
+    }
+
+    public static UserIdentityExtractor getX500NameExtractor(ASN1ObjectIdentifier identifier, Function<X509Certificate[],X500Name> x500Name) {
+        return new X500NameRDNExtractor(identifier, x500Name);
+    }
+
+    public static OrBuilder either(UserIdentityExtractor extractor) {
+        return new OrBuilder(extractor);
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityToModelMapper.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityToModelMapper.java
new file mode 100644
index 0000000..e86e1c2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityToModelMapper.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import java.util.List;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/30/2016
+ */
+
+public abstract class UserIdentityToModelMapper {
+
+    public abstract UserModel find(AuthenticationFlowContext context, Object userIdentity) throws Exception;
+
+    static class UsernameOrEmailMapper extends UserIdentityToModelMapper {
+
+        @Override
+        public UserModel find(AuthenticationFlowContext context, Object userIdentity) throws Exception {
+            return KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), userIdentity.toString().trim());
+        }
+    }
+
+    static class UserIdentityToCustomAttributeMapper extends UserIdentityToModelMapper {
+
+        private String _customAttribute;
+        UserIdentityToCustomAttributeMapper(String customAttribute) {
+            _customAttribute = customAttribute;
+        }
+
+        @Override
+        public UserModel find(AuthenticationFlowContext context, Object userIdentity) throws Exception {
+            KeycloakSession session = context.getSession();
+            List<UserModel> users = session.users().searchForUserByUserAttribute(_customAttribute, userIdentity.toString(), context.getRealm());
+            if (users != null && users.size() > 1) {
+                throw new ModelDuplicateException();
+            }
+            return users != null && users.size() == 1 ? users.get(0) : null;
+        }
+    }
+
+    public static UserIdentityToModelMapper getUsernameOrEmailMapper() {
+        return new UsernameOrEmailMapper();
+    }
+
+    public static UserIdentityToModelMapper getUserIdentityToCustomAttributeMapper(String attributeName) {
+        return new UserIdentityToCustomAttributeMapper(attributeName);
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java
new file mode 100644
index 0000000..e0860fa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.X509Certificate;
+
+import javax.ws.rs.core.Response;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.ServicesLogger;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/31/2016
+ */
+
+public class ValidateX509CertificateUsername extends AbstractX509ClientCertificateDirectGrantAuthenticator {
+
+    protected static ServicesLogger logger = ServicesLogger.LOGGER;
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+
+        X509Certificate[] certs = getCertificateChain(context);
+        if (certs == null || certs.length == 0) {
+            logger.debug("[ValidateX509CertificateUsername:authenticate] x509 client certificate is not available for mutual SSL.");
+            context.getEvent().error(Errors.USER_NOT_FOUND);
+            Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "X509 client certificate is missing.");
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+
+        X509AuthenticatorConfigModel config = null;
+        if (context.getAuthenticatorConfig() != null && context.getAuthenticatorConfig().getConfig() != null) {
+            config = new X509AuthenticatorConfigModel(context.getAuthenticatorConfig());
+        }
+        if (config == null) {
+            logger.warn("[ValidateX509CertificateUsername:authenticate] x509 Client Certificate Authentication configuration is not available.");
+            context.getEvent().error(Errors.USER_NOT_FOUND);
+            Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Configuration is missing.");
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+        // Validate X509 client certificate
+        try {
+            CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(config);
+            CertificateValidator validator = builder.build(certs);
+            validator.checkRevocationStatus()
+                    .validateKeyUsage()
+                    .validateExtendedKeyUsage();
+        } catch(Exception e) {
+            logger.error(e.getMessage(), e);
+            // TODO use specific locale to load error messages
+            Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", e.getMessage());
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+
+        Object userIdentity = getUserIdentityExtractor(config).extractUserIdentity(certs);
+        if (userIdentity == null) {
+            logger.errorf("[ValidateX509CertificateUsername:authenticate] Unable to extract user identity from certificate.");
+            // TODO use specific locale to load error messages
+            String errorMessage = "Unable to extract user identity from specified certificate";
+            Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", errorMessage);
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+        UserModel user;
+        try {
+            context.getEvent().detail(Details.USERNAME, userIdentity.toString());
+            context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
+            user = getUserIdentityToModelMapper(config).find(context, userIdentity);
+        }
+        catch(ModelDuplicateException e) {
+            logger.modelDuplicateException(e);
+            String errorMessage = String.format("X509 certificate authentication's failed. Reason: \"%s\"", e.getMessage());
+            Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", errorMessage);
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+        catch(Exception e) {
+            logger.error(e.getMessage(), e);
+            String errorMessage = String.format("X509 certificate authentication's failed. Reason: \"%s\"", e.getMessage());
+            Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", errorMessage);
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+        if (user == null) {
+            context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+            Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+        if (!user.isEnabled()) {
+            context.getEvent().user(user);
+            context.getEvent().error(Errors.USER_DISABLED);
+            Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account disabled");
+            context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+            return;
+        }
+        if (context.getRealm().isBruteForceProtected()) {
+            if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) {
+                context.getEvent().user(user);
+                context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
+                Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Account temporarily disabled");
+                context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
+                return;
+            }
+        }
+        context.setUser(user);
+        context.success();
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+        // Intentionally does nothing
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsernameFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsernameFactory.java
new file mode 100644
index 0000000..b26f268
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsernameFactory.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 7/31/2016
+ */
+
+public class ValidateX509CertificateUsernameFactory extends AbstractX509ClientCertificateAuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "direct-grant-auth-x509-username";
+    public static final ValidateX509CertificateUsername SINGLETON = new ValidateX509CertificateUsername();
+
+    @Override
+    public String getHelpText() {
+        return "Validates username and password from X509 client certificate received as a part of mutual SSL handshake.";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "X509/Validate Username";
+    }
+
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED
+    };
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return SINGLETON;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java
new file mode 100644
index 0000000..87c60b4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509AuthenticatorConfigModel.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import org.keycloak.models.AuthenticatorConfigModel;
+
+import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.*;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 10/26/2016
+ */
+
+public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
+
+    private static final long serialVersionUID = 1L;
+
+    public enum IdentityMapperType {
+        USER_ATTRIBUTE(USER_ATTRIBUTE_MAPPER),
+        USERNAME_EMAIL(USERNAME_EMAIL_MAPPER);
+
+        private String name;
+        IdentityMapperType(String name) {
+            this.name = name;
+        }
+        public String getName() {  return this.name; }
+        static public IdentityMapperType parse(String name) throws IllegalArgumentException, IndexOutOfBoundsException {
+            if (name == null || name.trim().length() == 0)
+                throw new IllegalArgumentException("name");
+
+            for (IdentityMapperType value : IdentityMapperType.values()) {
+                if (value.getName().equalsIgnoreCase(name))
+                    return value;
+            }
+            throw new IndexOutOfBoundsException("name");
+        }
+    }
+
+    public enum MappingSourceType {
+        SERIALNUMBER(MAPPING_SOURCE_CERT_SERIALNUMBER),
+        ISSUERDN_CN(MAPPING_SOURCE_CERT_ISSUERDN_CN),
+        ISSUERDN_EMAIL(MAPPING_SOURCE_CERT_ISSUERDN_EMAIL),
+        ISSUERDN(MAPPING_SOURCE_CERT_ISSUERDN),
+        SUBJECTDN_CN(MAPPING_SOURCE_CERT_SUBJECTDN_CN),
+        SUBJECTDN_EMAIL(MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL),
+        SUBJECTDN(MAPPING_SOURCE_CERT_SUBJECTDN);
+
+        private String name;
+        MappingSourceType(String name) {
+            this.name = name;
+        }
+        public String getName() {  return this.name; }
+        static public MappingSourceType parse(String name) throws IllegalArgumentException, IndexOutOfBoundsException {
+            if (name == null || name.trim().length() == 0)
+                throw new IllegalArgumentException("name");
+
+            for (MappingSourceType value : MappingSourceType.values()) {
+                if (value.getName().equalsIgnoreCase(name))
+                    return value;
+            }
+            throw new IndexOutOfBoundsException("name");
+        }
+    }
+
+    public X509AuthenticatorConfigModel(AuthenticatorConfigModel model) {
+        this.setAlias(model.getAlias());
+        this.setId(model.getId());
+        this.setConfig(model.getConfig());
+    }
+    public X509AuthenticatorConfigModel() {
+
+    }
+
+    public boolean getCRLEnabled() {
+        return Boolean.parseBoolean(getConfig().get(ENABLE_CRL));
+    }
+
+    public X509AuthenticatorConfigModel setCRLEnabled(boolean value) {
+        getConfig().put(ENABLE_CRL, Boolean.toString(value));
+        return this;
+    }
+
+    public boolean getOCSPEnabled() {
+        return Boolean.parseBoolean(getConfig().get(ENABLE_OCSP));
+    }
+
+    public X509AuthenticatorConfigModel setOCSPEnabled(boolean value) {
+        getConfig().put(ENABLE_OCSP, Boolean.toString(value));
+        return this;
+    }
+
+    public boolean getCRLDistributionPointEnabled() {
+        return Boolean.parseBoolean(getConfig().get(ENABLE_CRLDP));
+    }
+
+    public X509AuthenticatorConfigModel setCRLDistributionPointEnabled(boolean value) {
+        getConfig().put(ENABLE_CRLDP, Boolean.toString(value));
+        return this;
+    }
+
+    public String getCRLRelativePath() {
+        return getConfig().getOrDefault(CRL_RELATIVE_PATH, null);
+    }
+
+    public X509AuthenticatorConfigModel setCRLRelativePath(String path) {
+        if (path != null) {
+            getConfig().put(CRL_RELATIVE_PATH, path);
+        } else {
+            getConfig().remove(CRL_RELATIVE_PATH);
+        }
+        return this;
+    }
+
+    public String getOCSPResponder() {
+        return getConfig().getOrDefault(OCSPRESPONDER_URI, null);
+    }
+
+    public X509AuthenticatorConfigModel setOCSPResponder(String responderUri) {
+        if (responderUri != null) {
+            getConfig().put(OCSPRESPONDER_URI, responderUri);
+        } else {
+            getConfig().remove(OCSPRESPONDER_URI);
+        }
+        return this;
+    }
+
+    public MappingSourceType getMappingSourceType() {
+        return MappingSourceType.parse(getConfig().getOrDefault(MAPPING_SOURCE_SELECTION, MAPPING_SOURCE_CERT_SUBJECTDN));
+    }
+
+    public X509AuthenticatorConfigModel setMappingSourceType(MappingSourceType value) {
+        getConfig().put(MAPPING_SOURCE_SELECTION, value.getName());
+        return this;
+    }
+
+    public IdentityMapperType getUserIdentityMapperType() {
+        return IdentityMapperType.parse(getConfig().getOrDefault(USER_MAPPER_SELECTION, USERNAME_EMAIL_MAPPER));
+    }
+
+    public X509AuthenticatorConfigModel setUserIdentityMapperType(IdentityMapperType value) {
+        getConfig().put(USER_MAPPER_SELECTION, value.getName());
+        return this;
+    }
+
+    public String getRegularExpression() {
+        return getConfig().getOrDefault(REGULAR_EXPRESSION,DEFAULT_MATCH_ALL_EXPRESSION);
+    }
+
+    public X509AuthenticatorConfigModel setRegularExpression(String value) {
+        if (value != null) {
+            getConfig().put(REGULAR_EXPRESSION, value);
+        } else {
+            getConfig().remove(REGULAR_EXPRESSION);
+        }
+        return this;
+    }
+
+    public String getCustomAttributeName() {
+        return getConfig().getOrDefault(CUSTOM_ATTRIBUTE_NAME, DEFAULT_ATTRIBUTE_NAME);
+    }
+
+    public X509AuthenticatorConfigModel setCustomAttributeName(String value) {
+        if (value != null) {
+            getConfig().put(CUSTOM_ATTRIBUTE_NAME, value);
+        } else {
+            getConfig().remove(CUSTOM_ATTRIBUTE_NAME);
+        }
+        return this;
+    }
+
+    public String getKeyUsage() {
+        return getConfig().getOrDefault(CERTIFICATE_KEY_USAGE, null);
+    }
+
+    public X509AuthenticatorConfigModel setKeyUsage(String value) {
+        if (value != null) {
+            getConfig().put(CERTIFICATE_KEY_USAGE, value);
+        } else {
+            getConfig().remove(CERTIFICATE_KEY_USAGE);
+        }
+        return this;
+    }
+
+    public String getExtendedKeyUsage() {
+        return getConfig().getOrDefault(CERTIFICATE_EXTENDED_KEY_USAGE, null);
+    }
+
+    public X509AuthenticatorConfigModel setExtendedKeyUsage(String value) {
+        if (value != null) {
+            getConfig().put(CERTIFICATE_EXTENDED_KEY_USAGE, value);
+        } else {
+            getConfig().remove(CERTIFICATE_EXTENDED_KEY_USAGE);
+        }
+        return this;
+    }
+
+    public boolean getConfirmationPageDisallowed() {
+        return Boolean.parseBoolean(getConfig().get(CONFIRMATION_PAGE_DISALLOWED));
+    }
+
+    public boolean getConfirmationPageAllowed() {
+        return !Boolean.parseBoolean(getConfig().get(CONFIRMATION_PAGE_DISALLOWED));
+    }
+
+    public X509AuthenticatorConfigModel setConfirmationPageDisallowed(boolean value) {
+        getConfig().put(CONFIRMATION_PAGE_DISALLOWED, Boolean.toString(value));
+        return this;
+    }
+
+    public X509AuthenticatorConfigModel setConfirmationPageAllowed(boolean value) {
+        getConfig().put(CONFIRMATION_PAGE_DISALLOWED, Boolean.toString(!value));
+        return this;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java
new file mode 100644
index 0000000..21e67ec
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.X509Certificate;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.FormMessage;
+import org.keycloak.services.ServicesLogger;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ *
+ */
+public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertificateAuthenticator {
+
+    protected static ServicesLogger logger = ServicesLogger.LOGGER;
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+
+        try {
+
+            dumpContainerAttributes(context);
+
+            X509Certificate[] certs = getCertificateChain(context);
+            if (certs == null || certs.length == 0) {
+                // No x509 client cert, fall through and
+                // continue processing the rest of the authentication flow
+                logger.debug("[X509ClientCertificateAuthenticator:authenticate] x509 client certificate is not available for mutual SSL.");
+                context.attempted();
+                return;
+            }
+
+            X509AuthenticatorConfigModel config = null;
+            if (context.getAuthenticatorConfig() != null && context.getAuthenticatorConfig().getConfig() != null) {
+                config = new X509AuthenticatorConfigModel(context.getAuthenticatorConfig());
+            }
+            if (config == null) {
+                logger.warn("[X509ClientCertificateAuthenticator:authenticate] x509 Client Certificate Authentication configuration is not available.");
+                context.challenge(createInfoResponse(context, "X509 client authentication has not been configured yet"));
+                context.attempted();
+                return;
+            }
+
+            // Validate X509 client certificate
+            try {
+                CertificateValidator.CertificateValidatorBuilder builder = certificateValidationParameters(config);
+                CertificateValidator validator = builder.build(certs);
+                validator.checkRevocationStatus()
+                         .validateKeyUsage()
+                         .validateExtendedKeyUsage();
+            } catch(Exception e) {
+                logger.error(e.getMessage(), e);
+                // TODO use specific locale to load error messages
+                String errorMessage = "Certificate validation's failed.";
+                // TODO is calling form().setErrors enough to show errors on login screen?
+                context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(),
+                        errorMessage, e.getMessage()));
+                context.attempted();
+                return;
+            }
+
+            Object userIdentity = getUserIdentityExtractor(config).extractUserIdentity(certs);
+            if (userIdentity == null) {
+                logger.warnf("[X509ClientCertificateAuthenticator:authenticate] Unable to extract user identity from certificate.");
+                // TODO use specific locale to load error messages
+                String errorMessage = "Unable to extract user identity from specified certificate";
+                // TODO is calling form().setErrors enough to show errors on login screen?
+                context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(), errorMessage));
+                context.attempted();
+                return;
+            }
+
+            UserModel user;
+            try {
+                context.getEvent().detail(Details.USERNAME, userIdentity.toString());
+                context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
+                user = getUserIdentityToModelMapper(config).find(context, userIdentity);
+            }
+            catch(ModelDuplicateException e) {
+                logger.modelDuplicateException(e);
+                String errorMessage = "X509 certificate authentication's failed.";
+                // TODO is calling form().setErrors enough to show errors on login screen?
+                context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(),
+                        errorMessage, e.getMessage()));
+                context.attempted();
+                return;
+            }
+
+            if (invalidUser(context, user)) {
+                // TODO use specific locale to load error messages
+                String errorMessage = "X509 certificate authentication's failed.";
+                // TODO is calling form().setErrors enough to show errors on login screen?
+                context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(),
+                        errorMessage, "Invalid user"));
+                context.attempted();
+                return;
+            }
+
+            if (!userEnabled(context, user)) {
+                // TODO use specific locale to load error messages
+                String errorMessage = "X509 certificate authentication's failed.";
+                // TODO is calling form().setErrors enough to show errors on login screen?
+                context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(),
+                        errorMessage, "User is disabled"));
+                context.attempted();
+                return;
+            }
+            if (context.getRealm().isBruteForceProtected()) {
+                if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) {
+                    context.getEvent().user(user);
+                    context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
+                    // TODO use specific locale to load error messages
+                    String errorMessage = "X509 certificate authentication's failed.";
+                    // TODO is calling form().setErrors enough to show errors on login screen?
+                    context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(),
+                            errorMessage, "User is temporarily disabled. Contact administrator."));
+                    context.attempted();
+                    return;
+                }
+            }
+            context.setUser(user);
+
+            // Check whether to display the identity confirmation
+            if (!config.getConfirmationPageDisallowed()) {
+                // FIXME calling forceChallenge was the only way to display
+                // a form to let users either choose the user identity from certificate
+                // or to ignore it and proceed to a normal login screen. Attempting
+                // to call the method "challenge" results in a wrong/unexpected behavior.
+                // The question is whether calling "forceChallenge" here is ok from
+                // the design viewpoint?
+                context.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId());
+                context.forceChallenge(createSuccessResponse(context, certs[0].getSubjectDN().getName()));
+                // Do not set the flow status yet, we want to display a form to let users
+                // choose whether to accept the identity from certificate or to specify username/password explicitly
+            }
+            else {
+                // Bypass the confirmation page and log the user in
+                context.success();
+            }
+        }
+        catch(Exception e) {
+            logger.errorf("[X509ClientCertificateAuthenticator:authenticate] Exception: %s", e.getMessage());
+            context.attempted();
+        }
+    }
+
+    private Response createErrorResponse(AuthenticationFlowContext context,
+                                         String subjectDN,
+                                         String errorMessage,
+                                         String ... errorParameters) {
+
+        return createResponse(context, subjectDN, false, errorMessage, errorParameters);
+    }
+
+    private Response createSuccessResponse(AuthenticationFlowContext context,
+                                           String subjectDN) {
+        return createResponse(context, subjectDN, true, null, null);
+    }
+
+    private Response createResponse(AuthenticationFlowContext context,
+                                         String subjectDN,
+                                         boolean isUserEnabled,
+                                         String errorMessage,
+                                         Object[] errorParameters) {
+
+        LoginFormsProvider form = context.form();
+        if (errorMessage != null && errorMessage.trim().length() > 0) {
+            List<FormMessage> errors = new LinkedList<>();
+
+            errors.add(new FormMessage(errorMessage));
+            if (errorParameters != null) {
+
+                for (Object errorParameter : errorParameters) {
+                    if (errorParameter == null) continue;
+                    for (String part : errorParameter.toString().split("\n")) {
+                        errors.add(new FormMessage(part));
+                    }
+                }
+            }
+            form.setErrors(errors);
+        }
+
+        return form
+                .setAttribute("username", context.getUser() != null ? context.getUser().getUsername() : "unknown user")
+                .setAttribute("subjectDN", subjectDN)
+                .setAttribute("isUserEnabled", isUserEnabled)
+                .createForm("login-x509-info.ftl");
+    }
+
+    private void dumpContainerAttributes(AuthenticationFlowContext context) {
+
+        Enumeration<String> attributeNames = context.getHttpRequest().getAttributeNames();
+        while(attributeNames.hasMoreElements()) {
+            String a = attributeNames.nextElement();
+            logger.tracef("[X509ClientCertificateAuthenticator:dumpContainerAttributes] \"%s\"", a);
+        }
+    }
+
+    private boolean userEnabled(AuthenticationFlowContext context, UserModel user) {
+        if (!user.isEnabled()) {
+            context.getEvent().user(user);
+            context.getEvent().error(Errors.USER_DISABLED);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean invalidUser(AuthenticationFlowContext context, UserModel user) {
+        if (user == null) {
+            context.getEvent().error(Errors.USER_NOT_FOUND);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+        if (formData.containsKey("cancel")) {
+            context.clearUser();
+            context.attempted();
+            return;
+        }
+        if (context.getUser() != null) {
+            context.success();
+            return;
+        }
+        context.attempted();
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticatorFactory.java
new file mode 100644
index 0000000..b36da5e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticatorFactory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016 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.authentication.authenticators.x509;
+
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ *
+ */
+public class X509ClientCertificateAuthenticatorFactory  extends AbstractX509ClientCertificateAuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "auth-x509-client-username-form";
+    public static final X509ClientCertificateAuthenticator SINGLETON =
+            new X509ClientCertificateAuthenticator();
+
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.ALTERNATIVE,
+            AuthenticationExecutionModel.Requirement.DISABLED
+    };
+
+
+    @Override
+    public String getHelpText() {
+        return "Validates username and password from X509 client certificate received as a part of mutual SSL handshake.";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "X509/Validate Username Form";
+    }
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return SINGLETON;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index fa7ee28..208f16d 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -34,4 +34,6 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac
 org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
 org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
 org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
-org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator
\ No newline at end of file
+org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator
+org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
+org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index 90f7a28..edaf038 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -295,4 +295,16 @@ To run the tests run:
 #### Internet Explorer
 * **Supported version:** 11
 * **Driver download required:** [Internet Explorer Driver Server](http://www.seleniumhq.org/download/); recommended version [2.53.1 32-bit](http://selenium-release.storage.googleapis.com/2.53/IEDriverServer_Win32_2.53.1.zip)
-* **Run with:** `-Dbrowser=internetExplorer -Dwebdriver.ie.driver=path/to/IEDriverServer.exe`
\ No newline at end of file
+* **Run with:** `-Dbrowser=internetExplorer -Dwebdriver.ie.driver=path/to/IEDriverServer.exe`
+ 
+## Run X.509 tests
+
+To run the X.509 client certificate authentication tests:
+
+    mvn -f testsuite/integration-arquillian/pom.xml \
+          clean install \
+	  -Pauth-server-wildfly \
+	  -Dauth.server.ssl.required \
+	  -Dbrowser=phantomjs \
+	  "-Dtest=*.x509.*"
+
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ca.crt b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ca.crt
new file mode 100644
index 0000000..d37a76e
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ca.crt
@@ -0,0 +1,26 @@
+Bag Attributes
+    friendlyName: localhost
+    localKeyID: 54 69 6D 65 20 31 34 37 37 32 37 36 33 32 32 32 32 35 
+subject=/C=US/ST=MA/L=Westword/O=Red Hat/OU=Keycloak/CN=localhost
+issuer=/C=US/ST=MA/L=Westword/O=Red Hat/OU=Keycloak/CN=localhost
+-----BEGIN CERTIFICATE-----
+MIIDazCCAlOgAwIBAgIERfv3izANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCTUExETAPBgNVBAcTCFdlc3R3b3JkMRAwDgYDVQQKEwdSZWQg
+SGF0MREwDwYDVQQLEwhLZXljbG9hazESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE1
+MTIwNDA2NTExOFoXDTQ1MTEyNjA2NTExOFowZjELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAk1BMREwDwYDVQQHEwhXZXN0d29yZDEQMA4GA1UEChMHUmVkIEhhdDERMA8G
+A1UECxMIS2V5Y2xvYWsxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAIb7QEw18tpTIVoLUS8kpZaU84btm4nkbVrVNOxC
+zsOVfhFGsc6kUamhHokvvOSWqHS+5FOTVWHPYrNTIwm1vodkqiy7xLCC8MWTrtU5
+RwcrCZ8Mwkm0EUCLCTY113j9egIg+Uj4nkQyTPGNliygf+ef3finzUfarc1lBAHD
++Z7cjrx4odtvQu88oGdhEXv5GoIno4bwkLRJKWWw9MRZGBxdTJlRGJ2hr0FVtNTw
+sMvgR6ZeDosH8zNNLikLuwMAl7qxCgzppfmZCGKF2H/JLaXUo1oCIwdtCSSJufGJ
+sa9cjdehroVIaiVaASQDKVUStoFz4kYrqUzOves4waJsRvcCAwEAAaMhMB8wHQYD
+VR0OBBYEFFCfEXmWKTtaiZG7tCvBrmQiujrLMA0GCSqGSIb3DQEBCwUAA4IBAQAD
+j/o+snjk/pydFLd3T6gr7k+ZWBi0gQKOOZ+xO9opblYMtG4bRm7wqsTyheUyeTQT
+DZNXIFN4fgCcvHpEi+3M9XL8gySVsu7XzN49UT+KXavwISlbWyryZDH42L/MNCjG
+Z8CD4IsyPAawgrC2Pc8NH8De5YqsGn2DId6R6xjFEumYtAEXXe3Wcp9T4G6yWSXO
+s0rARNfE534Rvne7Gx18g/Lj0BBP7qh3bNeReRmHKpnRK/V90SJNOkpaFF4oAMQr
+0pcZTJa4zoNcAoLHnwNBZmq43cPrffEOOMaCadiSSQ6bsJ0adZ+MSeJ1j4C9SrUn
+M9ES3g9Wj9OcCsHzrTAm
+-----END CERTIFICATE-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.crt b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.crt
new file mode 100644
index 0000000..15b1356
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.crt
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEdDCCA1ygAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xEDAOBgNVBAoMB1JlZCBIYXQx
+ETAPBgNVBAsMCEtleWNsb2FrMREwDwYDVQQDDAhLZXljbG9hazAgFw0xNjEwMzEy
+MDM4NDNaGA8zMDE2MDMwMzIwMzg0M1owgZQxCzAJBgNVBAYTAlVTMQswCQYDVQQI
+DAJNQTERMA8GA1UEBwwIV2VzdHdvb2QxEDAOBgNVBAoMB1JlZCBIYXQxETAPBgNV
+BAsMCEtleWNsb2FrMRwwGgYDVQQDDBN0ZXN0LXVzZXJAbG9jYWxob3N0MSIwIAYJ
+KoZIhvcNAQkBFhN0ZXN0LXVzZXJAbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA0JQtc138gSkvEXOywfAbM2NW60I2//LyMSbjn1pg2H+O
+illwHd9KvB8tGC42HIClsUuNYQw623xVffD2CMvDW/5rVGTOhn7MWSWpRD6iMma8
+fQM8NfLou4TGFzD+HUqPcje3mwm0OxWmVnZ+zF3M68aesy4Qhbv201ND1PiQjzFH
+S/05mCV5SDlTRKf4iLszsV03SDc3zqSSAU7dbiLRQFlJ9PX6EAlkZhq+vcoa210s
+k5bKGh0D7j+m9rqATsQP9voq0TxN/HibLeQ6rJbraD2Zk++Z6XXGww7wdxV3gGW3
+1DOq6fC4QVF76qOXtons9rR0tReD5KC5vPzmW6No/wIDAQABo4H9MIH6MAkGA1Ud
+EwQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMDMGCWCGSAGG+EIBDQQmFiRPcGVuU1NM
+IEdlbmVyYXRlZCBDbGllbnQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFMLdiOrXFhFF
+sUcG4qYGwC2qkEOvMB8GA1UdIwQYMBaAFJ1G0xr/t9MQ/8lyXdmgbdoxAoOjMA4G
+A1UdDwEB/wQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwNgYI
+KwYBBQUHAQEEKjAoMCYGCCsGAQUFBzABhhpodHRwOi8vbG9jYWxob3N0Ojg4ODgv
+b3NjcDANBgkqhkiG9w0BAQsFAAOCAQEAK2v0XgHQcwSufq9Ax2841PfKo82vHiZu
+kKnT/rAc2VtG/A3ppMdwavFiMfWD9lLmx0+vSODVtcGVLqIbsd0VLrWxfs1CL214
+Ae1skj8gDBrEdfCwq0CQNJpcrBDHvFeq7PuFNNNT0ZDnoZ4FWGfcu+UFOgU5kWsW
+3ostYQczLRqiXklTnRnqRLA31tI/ImgPKE0gNc5SxcpOJsg0poUIprYT1Bdj7ZKV
+wlwN7HVjUGEVcPku9dGkwCRQxRlrL1z61IX79K538AWGHs/1mHyN68GQ1NYVe+y5
++WES72t68nXFtLDMTNcDik0VKpwnpuyS++KVCX/6fQc04gN4fo8yyw==
+-----END CERTIFICATE-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.jks b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.jks
new file mode 100644
index 0000000..b2ac2cf
Binary files /dev/null and b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.jks differ
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.key b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.key
new file mode 100644
index 0000000..53b5a40
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/client.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA0JQtc138gSkvEXOywfAbM2NW60I2//LyMSbjn1pg2H+Oillw
+Hd9KvB8tGC42HIClsUuNYQw623xVffD2CMvDW/5rVGTOhn7MWSWpRD6iMma8fQM8
+NfLou4TGFzD+HUqPcje3mwm0OxWmVnZ+zF3M68aesy4Qhbv201ND1PiQjzFHS/05
+mCV5SDlTRKf4iLszsV03SDc3zqSSAU7dbiLRQFlJ9PX6EAlkZhq+vcoa210sk5bK
+Gh0D7j+m9rqATsQP9voq0TxN/HibLeQ6rJbraD2Zk++Z6XXGww7wdxV3gGW31DOq
+6fC4QVF76qOXtons9rR0tReD5KC5vPzmW6No/wIDAQABAoIBAQDNPLEIbzfyQ/sL
+miQQT1oEk8BIV+xUp9MG6HRcLgtTnRg+LEl1K22XdLnjrD0UiMYBkg04rFcR9+ML
+vNnrx8ygrdUgwtyxXP1ozvUisNra61qXTGKn1zCIq9pboZq6ac6Cni6tgLv205PM
+Smb0Y2wrqFIqj640WcNDKBB79GYSbq0oQKOxCXUI7Y+vv521X5Y0CCymDpeghPVv
+zC63bdH+IPsuKUHJJvWSTi3YhWPWgQ4qRk3E1PEBKK4La1qnZ+kCLn18Oq9n1r8s
+kUr+Wremnyvf4smnYpdqF/r3ZFUXEVRwhGpSE8yhxewtnurg9iJz6D5DKl5PR3UR
+T0PxIznxAoGBAOmQRtams3I5Ed+UdLJRjBnGj9Yts5GfmzlMPlaErjsEDGYaumQP
+sGyy4PfS6zGqI9EANR2q6Ei7eNs3zt233jvN0L7HobTyfvc1e8usmgsGmKXnKELx
+0QzmR7mxZ8d92goM2fOCVWJt8IeElZxlAUFlV7WbHJHrMrSKhHl7sBiXAoGBAOSd
+e7iby+tVJ7IShNkEvh1cSwxfFF1/E71WqRJ5ujotH7p8lmxF827sf7ZWOpeCoASS
+isPEr0iJ1t1jHAbOzFM9zYC3n397u6DaUawsAIVPHzs89n6nFmPRg6nqHrarb08F
+N13P/vdewolNq/k3CRNQcALfRcAGGi5OJoP8PxfZAoGAXVyrxF/seSsw5tN79k+8
+lg0WMAIitoI7VSMSBBmTX8nVfvcOLLNMzZaPcODh3P9qw7+2e752jaRKwi5DxcMa
+1YMG/0vjauZINF8vthGvndqsKB0HrgLlkqd2Ylb06fx3rO7ILJubr/XMYQ9xkouZ
+fd+ZuOFhAT8oDJ0WKU107c8CgYEAnw3iMgJS0ZwhTgcMXmCqVIsNu8G6zYHIHgbZ
+An2mTytt07Uygon4X8gy4o0Dnr3GzueVm7aPrQwZbVWy/hmKlq989nzKf0t6suKx
+x/OlORpfRDuZ02w0a6Ys0F6Ol9CWVIfwiVhYr4pCFnzvlSfxLHreM2Fbicm1FVpR
++anZy6ECgYEA2w7y3z0WpUDiulDHnVT0qGaurHCjA8bNZdDVzAKIYlXP9NVP1fHV
+v+HIaViI2OH0knHVo+xybCqw9iREHw31pFbqKhLseoEsLgyp29532Ubug4xiaGdu
+1syDZbIVO+7b19lmJWxOkCzIF/7JPcnls7T1NbcGpMNMaZDUvugFde4=
+-----END RSA PRIVATE KEY-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/empty.crl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/empty.crl
new file mode 100644
index 0000000..5b85784
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/empty.crl
@@ -0,0 +1,12 @@
+-----BEGIN X509 CRL-----
+MIIBvzCBqAIBATANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJVUzELMAkGA1UE
+CBMCTUExETAPBgNVBAcTCFdlc3R3b3JkMRAwDgYDVQQKEwdSZWQgSGF0MREwDwYD
+VQQLEwhLZXljbG9hazESMBAGA1UEAxMJbG9jYWxob3N0Fw0xNjEwMjUxNTQ1MTla
+Fw0xNjExMjQxNTQ1MTlaoA4wDDAKBgNVHRQEAwIBATANBgkqhkiG9w0BAQsFAAOC
+AQEAEOvgtvCPNjLTS9AMlv4AzhASQ3jbgFBhdNK6YuOSfvOk8t5YonxZACJy+3PR
+rK2aFl6GT4FhIPT4duK/r1s+VhlRKfTAjs3WYq2ftdYyBlZBjDFyoRHHmtblnY+H
+qRuaZRNGzPMjooLyCM0ZKVSoBgLrllzU8HCY+5kJ0lmo2/A83XORsfQ+pRm8vNOf
+l6Q17lS44ApOlUxLXux1gW5YEfJ6pnJkd2QCWJ5DOFKGrFZnm2CYA5baQS9SpxA/
+lqYVlC8oEDY6YAng1QYnGCPLHM0ONeYmdzB5p1xRbmXJLAqEQY6DqhNtWQAgKJy3
+ArqBs/g6CuuoPvwGogykmSI4AA==
+-----END X509 CRL-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca.crl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca.crl
new file mode 100644
index 0000000..10eb8fb
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/intermediate-ca.crl
@@ -0,0 +1,13 @@
+-----BEGIN X509 CRL-----
+MIIB9TCB3gIBATANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzELMAkGA1UE
+CAwCTUExDzANBgNVBAcMBkJvc3RvbjEQMA4GA1UECgwHUmVkIEhhdDERMA8GA1UE
+CwwIS2V5Y2xvYWsxETAPBgNVBAMMCEtleWNsb2FrFw0xNjEwMzEyMDQzNDBaFw0x
+NjExMzAyMDQzNDBaMBUwEwICEAAXDTE2MTAzMTIwNDI1MlqgMDAuMB8GA1UdIwQY
+MBaAFJ1G0xr/t9MQ/8lyXdmgbdoxAoOjMAsGA1UdFAQEAgIQATANBgkqhkiG9w0B
+AQsFAAOCAQEA5LZMg0uxFh2FtPEYeIrX4rKZ06Q7re5tE99U/Ua7KAXMyOhcz7ba
+wAQbC7qN5RxD34NHAUTKV+QYHNfu1569xsfB60YVdYHcecV+jbzvecPTJVxFDprU
+5puw0NqVUb0xPKqdJGhYqkPiATppsqggbmwgKJnLTy207K5KboOdRsCag4XSZLul
+9PttdON62HFPJfNGsbpfGleRGpM0C3uju+nUnqocaSIf11DNOpUpgKH4HqKv3yIM
+TY7R8fvSX0QxcUtA+CJg6eS+XArguD1PWb0PwEy1psrgxrazwUysl0FVnw2oYDhO
+PPLMXuNvkMQ5bMsTOHgX3ohIyLbAVfDhAA==
+-----END X509 CRL-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/keycloak.truststore b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/keycloak.truststore
index da0f709..5253c8f 100644
Binary files a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/keycloak.truststore and b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/keycloak.truststore differ
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/certs/ca-chain.crt b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/certs/ca-chain.crt
new file mode 100644
index 0000000..7adcbe6
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/certs/ca-chain.crt
@@ -0,0 +1,44 @@
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgTAk1BMREwDwYDVQQHEwhXZXN0d29yZDEQMA4GA1UEChMHUmVkIEhh
+dDERMA8GA1UECxMIS2V5Y2xvYWsxEjAQBgNVBAMTCWxvY2FsaG9zdDAgFw0xNjEw
+MzEyMDA2NTJaGA8zMDE2MDMwMzIwMDY1MlowYzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xEDAOBgNVBAoMB1JlZCBIYXQxETAPBgNV
+BAsMCEtleWNsb2FrMREwDwYDVQQDDAhLZXljbG9hazCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAOyWSWjty1OtggI8zI+tbLy0mdyE5z3pnqUuspLZcrb3
+sWoSWh0m+OhZOH8wIldav3+nNtM3G5dLK8L+iMRFu/1clpIDtzoLSV/v0HxiovJi
+rOW9PhfyMu+vQE3D27zgOspW6leETQhO6tNKKT/NqBkgAIyYp0zAGT3tVs3k7mL+
+jVorVf8lbExOaomH3S2HoxFUHsMUBkS0WYV2tN1Fyqp9ieVGBu0fiItnZAOofi6G
+kS1L2ZBRbHW4eRbr77gUsETgPYXkPdN0mT7KC90KLEJfie7TyQf7O7EACQb+Y5nG
+ySBT93fDTaIbJ2JR4UYwLXiLGZF4k+mvlQj2PJAG1W0CAwEAAaN+MHwwHQYDVR0O
+BBYEFJ1G0xr/t9MQ/8lyXdmgbdoxAoOjMB8GA1UdIwQYMBaAFFCfEXmWKTtaiZG7
+tCvBrmQiujrLMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBYG
+A1UdJQEB/wQMMAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQAbcogxp/yb
+i7gF1Z+mZUwhqd2hqmMlE2AmLrZuo5jsq66XhwQTk13kvUYExlQw6DStrKDuRySj
+DRIQhu5UU4hLhewa9yl8iRs/zvZQsShJZ0vJPBhisTzU5vnA+ioHqTF91DX66xHE
+l57O8Vcmt8fCAg+LpWTokNvy04/0+vmy0od1/LF9sawTgXYu5g+o4JYzurOMxH9v
+Kn6cOLvpiAO/BsCYXKZxTu6WQX2N4AVRDVBJSiTvHylXtQX/t7fMIm27qU/TYona
+jWmZjy8Up99LpL+q6L4W8zZc35jzKF64Th6lMz/shc4/BHa1QVPgQ9yRWnoNJvUz
+9IceMNNiEnA5
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDazCCAlOgAwIBAgIERfv3izANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCTUExETAPBgNVBAcTCFdlc3R3b3JkMRAwDgYDVQQKEwdSZWQg
+SGF0MREwDwYDVQQLEwhLZXljbG9hazESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE1
+MTIwNDA2NTExOFoXDTQ1MTEyNjA2NTExOFowZjELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAk1BMREwDwYDVQQHEwhXZXN0d29yZDEQMA4GA1UEChMHUmVkIEhhdDERMA8G
+A1UECxMIS2V5Y2xvYWsxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAIb7QEw18tpTIVoLUS8kpZaU84btm4nkbVrVNOxC
+zsOVfhFGsc6kUamhHokvvOSWqHS+5FOTVWHPYrNTIwm1vodkqiy7xLCC8MWTrtU5
+RwcrCZ8Mwkm0EUCLCTY113j9egIg+Uj4nkQyTPGNliygf+ef3finzUfarc1lBAHD
++Z7cjrx4odtvQu88oGdhEXv5GoIno4bwkLRJKWWw9MRZGBxdTJlRGJ2hr0FVtNTw
+sMvgR6ZeDosH8zNNLikLuwMAl7qxCgzppfmZCGKF2H/JLaXUo1oCIwdtCSSJufGJ
+sa9cjdehroVIaiVaASQDKVUStoFz4kYrqUzOves4waJsRvcCAwEAAaMhMB8wHQYD
+VR0OBBYEFFCfEXmWKTtaiZG7tCvBrmQiujrLMA0GCSqGSIb3DQEBCwUAA4IBAQAD
+j/o+snjk/pydFLd3T6gr7k+ZWBi0gQKOOZ+xO9opblYMtG4bRm7wqsTyheUyeTQT
+DZNXIFN4fgCcvHpEi+3M9XL8gySVsu7XzN49UT+KXavwISlbWyryZDH42L/MNCjG
+Z8CD4IsyPAawgrC2Pc8NH8De5YqsGn2DId6R6xjFEumYtAEXXe3Wcp9T4G6yWSXO
+s0rARNfE534Rvne7Gx18g/Lj0BBP7qh3bNeReRmHKpnRK/V90SJNOkpaFF4oAMQr
+0pcZTJa4zoNcAoLHnwNBZmq43cPrffEOOMaCadiSSQ6bsJ0adZ+MSeJ1j4C9SrUn
+M9ES3g9Wj9OcCsHzrTAm
+-----END CERTIFICATE-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/certs/intermediate-ca.crt b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/certs/intermediate-ca.crt
new file mode 100644
index 0000000..73ad33f
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/certs/intermediate-ca.crt
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgTAk1BMREwDwYDVQQHEwhXZXN0d29yZDEQMA4GA1UEChMHUmVkIEhh
+dDERMA8GA1UECxMIS2V5Y2xvYWsxEjAQBgNVBAMTCWxvY2FsaG9zdDAgFw0xNjEw
+MzEyMDA2NTJaGA8zMDE2MDMwMzIwMDY1MlowYzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xEDAOBgNVBAoMB1JlZCBIYXQxETAPBgNV
+BAsMCEtleWNsb2FrMREwDwYDVQQDDAhLZXljbG9hazCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAOyWSWjty1OtggI8zI+tbLy0mdyE5z3pnqUuspLZcrb3
+sWoSWh0m+OhZOH8wIldav3+nNtM3G5dLK8L+iMRFu/1clpIDtzoLSV/v0HxiovJi
+rOW9PhfyMu+vQE3D27zgOspW6leETQhO6tNKKT/NqBkgAIyYp0zAGT3tVs3k7mL+
+jVorVf8lbExOaomH3S2HoxFUHsMUBkS0WYV2tN1Fyqp9ieVGBu0fiItnZAOofi6G
+kS1L2ZBRbHW4eRbr77gUsETgPYXkPdN0mT7KC90KLEJfie7TyQf7O7EACQb+Y5nG
+ySBT93fDTaIbJ2JR4UYwLXiLGZF4k+mvlQj2PJAG1W0CAwEAAaN+MHwwHQYDVR0O
+BBYEFJ1G0xr/t9MQ/8lyXdmgbdoxAoOjMB8GA1UdIwQYMBaAFFCfEXmWKTtaiZG7
+tCvBrmQiujrLMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBYG
+A1UdJQEB/wQMMAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQAbcogxp/yb
+i7gF1Z+mZUwhqd2hqmMlE2AmLrZuo5jsq66XhwQTk13kvUYExlQw6DStrKDuRySj
+DRIQhu5UU4hLhewa9yl8iRs/zvZQsShJZ0vJPBhisTzU5vnA+ioHqTF91DX66xHE
+l57O8Vcmt8fCAg+LpWTokNvy04/0+vmy0od1/LF9sawTgXYu5g+o4JYzurOMxH9v
+Kn6cOLvpiAO/BsCYXKZxTu6WQX2N4AVRDVBJSiTvHylXtQX/t7fMIm27qU/TYona
+jWmZjy8Up99LpL+q6L4W8zZc35jzKF64Th6lMz/shc4/BHa1QVPgQ9yRWnoNJvUz
+9IceMNNiEnA5
+-----END CERTIFICATE-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/index.txt b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/index.txt
new file mode 100644
index 0000000..e10de46
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/index.txt
@@ -0,0 +1 @@
+R	30160303203843Z	161031204252Z	1000	unknown	/C=US/ST=MA/L=Westwood/O=Red Hat/OU=Keycloak/CN=test-user@localhost/emailAddress=test-user@localhost
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/index.txt.attr b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/index.txt.attr
new file mode 100644
index 0000000..8f7e63a
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/index.txt.attr
@@ -0,0 +1 @@
+unique_subject = yes
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/private/intermediate-ca.key b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/private/intermediate-ca.key
new file mode 100644
index 0000000..d1dc6cf
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/keystore/ocsp/private/intermediate-ca.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA7JZJaO3LU62CAjzMj61svLSZ3ITnPemepS6yktlytvexahJa
+HSb46Fk4fzAiV1q/f6c20zcbl0srwv6IxEW7/VyWkgO3OgtJX+/QfGKi8mKs5b0+
+F/Iy769ATcPbvOA6ylbqV4RNCE7q00opP82oGSAAjJinTMAZPe1WzeTuYv6NWitV
+/yVsTE5qiYfdLYejEVQewxQGRLRZhXa03UXKqn2J5UYG7R+Ii2dkA6h+LoaRLUvZ
+kFFsdbh5FuvvuBSwROA9heQ903SZPsoL3QosQl+J7tPJB/s7sQAJBv5jmcbJIFP3
+d8NNohsnYlHhRjAteIsZkXiT6a+VCPY8kAbVbQIDAQABAoIBAAPhdzTMacBmoTJO
+MwDMVHNH9xoh5/UhBuQovu8ft/z+VR+rS2UdBYoyJHYZoQaTy35ZjNGsHry9k+sv
+56hoiGvgf+vHOdMu6jYzpdTUfV3CdpmSIBmedG1wD3r3EnynpO86u79RwT24patv
+lPuh8PbwinHD80KUCBX29ayM68gd4rptebF+BW7iN1yvw4lgPLbCAgjRgRee2r7i
+kTDQKgqSV9oZObJB10Lf42E/COXzAj2gJo55bjQFoZtpaueaLZNn83hKNsEFOwYi
+Xe9I5PGs0xBltiWGuodmUMgat4BS25Bg398rVZhC8HC79NfLeD6XRMWS2muVM8eR
+/A61TGECgYEA+uDFw/cH/sYx32JGcTRHGdbwNDI9edIB5B/WP6D21pql7LZWPGa2
+uSNP1fDJwbqEGKxHLGOJYBHySao8Vnjl38ppwj2pPQAqnhQW7gy2Re/tl8y/2GF8
+EMFn3o7/Nb85NdydBlUU0Lf3MpI0C8GEQ0EA6KeRrb0H2q77/KAQNUUCgYEA8WrS
+XxPaDVaRLGWePjey0laOl7wNGpZuQ3MgUiN1FyGcdFs2VEr8GFhW1umSvDgFSHyj
+eN+gmBHkwWTSSqfxzukSfhUbFQLrqtOvhyeHC5MJOf+Z4hE2haLzVIT5y7sIe2Tz
+IbScrwUpRDDx4QIYvD3Y7cu/XpG3W90/oxOZfgkCgYEAsSJ9YGUOdxv3YLMh65Iu
+1ZbGWQRUFaxq+2hjPN/pCRN+Lgl8D/+x6jx9hSATRD7uQAF762KIP5XMTajuG1wX
+apCfZa9lzBVPAfhLTF8bX3wNdM3zctM55xa3wROULJ3MxjPzhuIR5WDPIBzMtLQ5
+L8c43gxatqIuZoCAmRlm+EUCgYEAnvgbbRYyaMQPNm/3wrqEmzL6FHp1GoPiObIO
+n2fPy5qLXqdNYDY8SH+rNzt5L02oVPIV5xtCscBepOaR/y0V8ozRa4rUWYKh0swv
+8kyaBYod8j4Yg5+YCmmh1470ui3/yNlRrGk1H57DbK7aJ7NsvE/xj6yx6EEFv40y
+QV0/npkCgYEAxqrBDuoWx71zghC33g+lE73G7x21M/565rc+UOMOgyCCBUvc74DE
+JXe6/hpPc4wF5yLosJ+pkzT1YlTTzwEiYwpwE73DxrZRmnPHPqUMQDyfHRg8HGs3
+hJQ/jdKyqtwxmEoCAcaq5p+sa9WuxrvLqfquvEkeL2xNAKrkq/Iw4Ug=
+-----END RSA PRIVATE KEY-----
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/security-wildfly.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/security-wildfly.xsl
index ec6b9de..d51a2d8 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/security-wildfly.xsl
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/security-wildfly.xsl
@@ -39,6 +39,9 @@
                         <keystore path="keycloak.jks" relative-to="jboss.server.config.dir" keystore-password="secret"/>
                     </ssl>
                 </server-identities>
+                <authentication>
+                    <truststore path="keycloak.truststore" relative-to="jboss.server.config.dir" keystore-password="secret"/>
+                </authentication>
             </security-realm>
         </xsl:copy>
     </xsl:template>
@@ -46,7 +49,7 @@
         <http-listener name="default" socket-binding="http" redirect-socket="proxy-https" proxy-address-forwarding="true"/>
     </xsl:template>
     <xsl:template match="//u:host">
-        <https-listener name="https" socket-binding="proxy-https" security-realm="UndertowRealm"/>
+        <https-listener name="https" socket-binding="proxy-https" verify-client="REQUESTED" security-realm="UndertowRealm"/>
         <xsl:copy-of select="."/>
     </xsl:template>
 
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
index 85b6be4..0d8ac58 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/pom.xml
@@ -236,6 +236,12 @@
                                             <includes>
                                                 <include>keycloak.jks</include>
                                                 <include>keycloak.truststore</include>
+                                                <include>client.jks</include>
+                                                <include>ca.crt</include>
+                                                <include>client.crt</include>
+                                                <include>client.key</include>
+                                                <include>intermediate-ca.crl</include>
+                                                <include>empty.crl</include>
                                             </includes>
                                         </resource>
                                     </resources>
@@ -343,6 +349,10 @@
                     <name>auth.server.ssl.required</name>
                 </property>
             </activation>
+            <properties>
+                <!--disable exclusion pattern for x509 tests, which is enabled by default in the base/pom.xml-->
+                <exclude.x509>-</exclude.x509>
+            </properties>
             <build>
                 <pluginManagement>
                     <plugins>
@@ -388,6 +398,12 @@
                                                 <includes>
                                                     <include>keycloak.jks</include>
                                                     <include>keycloak.truststore</include>
+                                                    <include>client.jks</include>
+                                                    <include>ca.crt</include>
+                                                    <include>client.crt</include>
+                                                    <include>client.key</include>
+                                                    <include>intermediate-ca.crl</include>
+                                                    <include>empty.crl</include>
                                                 </includes>
                                             </resource>
                                         </resources>
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 62c6bfe..02cd878 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -40,7 +40,8 @@
         <exclude.client>-</exclude.client>
         <!--exclude cluster tests by default, enabled by 'auth-server-*-cluster' profiles in tests/pom.xml-->
         <exclude.cluster>**/cluster/**/*Test.java</exclude.cluster>
-
+        <!-- exclude x509 tests by default, enabled by 'ssl' profile -->
+        <exclude.x509>**/x509/*Test.java</exclude.x509>
         <!-- exclude undertow adapter tests. They can be added by -Dtest=org.keycloak.testsuite.adapter.undertow.**.*Test -->
         <exclude.undertow.adapter>**/adapter/undertow/**/*Test.java</exclude.undertow.adapter>
     </properties>
@@ -124,6 +125,7 @@
                         <exclude>${exclude.client}</exclude>
                         <exclude>${exclude.cluster}</exclude>
                         <exclude>${exclude.undertow.adapter}</exclude>
+                        <exclude>${exclude.x509}</exclude>
                     </excludes>
                 </configuration>
             </plugin>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
index 1e20944..684c383 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
@@ -26,6 +26,9 @@ import org.keycloak.testsuite.client.resources.TestingResource;
 import org.keycloak.testsuite.runonserver.*;
 import org.keycloak.util.JsonSerialization;
 
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
 /**
  * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
  */
@@ -33,12 +36,22 @@ public class KeycloakTestingClient {
 
     private final ResteasyWebTarget target;
     private final ResteasyClient client;
+    private static final boolean authServerSslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
 
     KeycloakTestingClient(String serverUrl, ResteasyClient resteasyClient) {
-        client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
+        client = resteasyClient != null ? resteasyClient : newResteasyClientBuilder().connectionPoolSize(10).build();
         target = client.target(serverUrl);
     }
 
+    private static ResteasyClientBuilder newResteasyClientBuilder() {
+        if (authServerSslRequired) {
+            // Disable PKIX path validation errors when running tests using SSL
+            HostnameVerifier hostnameVerifier = (hostName, session) -> true;
+            return new ResteasyClientBuilder().disableTrustManager().hostnameVerifier(hostnameVerifier);
+        }
+        return new ResteasyClientBuilder();
+    }
+
     public static KeycloakTestingClient getInstance(String serverUrl) {
         return new KeycloakTestingClient(serverUrl, null);
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/x509/X509IdentityConfirmationPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/x509/X509IdentityConfirmationPage.java
new file mode 100644
index 0000000..a656d50
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/x509/X509IdentityConfirmationPage.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 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.testsuite.pages.x509;
+
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.pages.AbstractPage;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 10/24/2016
+ */
+
+public class X509IdentityConfirmationPage extends AbstractPage {
+
+    @ArquillianResource
+    protected OAuthClient oauth;
+
+    @FindBy(id = "username")
+    private WebElement usernameText;
+
+    @FindBy(name = "login")
+    private WebElement confirmButton;
+
+    @FindBy(name = "cancel")
+    private WebElement ignoreButton;
+
+    @FindBy(className = "alert-error")
+    private WebElement loginErrorMessage;
+
+    @FindBy(className = "alert-warning")
+    private WebElement loginWarningMessage;
+
+    @FindBy(className = "alert-success")
+    private WebElement loginSuccessMessage;
+
+    @FindBy(className = "alert-info")
+    private WebElement loginInfoMessage;
+
+    @FindBy(id = "counter")
+    private WebElement loginDelayCounter;
+
+    @FindBy(id = "certificate_subjectDN")
+    private WebElement certificateSubjectDistinguishedName;
+
+    public void confirm() {
+        confirmButton.click();
+    }
+
+    public String getLoginDelayCounterText() {return loginDelayCounter.getText(); }
+
+    public String getSubjectDistinguishedNameText() { return certificateSubjectDistinguishedName.getText(); }
+
+    public String getUsernameText() { return usernameText.getText(); }
+
+    public void ignore() {
+        ignoreButton.click();
+    }
+
+    public String getError() {
+        return loginErrorMessage != null ? loginErrorMessage.getText() : null;
+    }
+
+    public boolean isCurrent() {
+        return driver.getTitle().equals("Log in to test") || driver.getTitle().equals("Anmeldung bei test");
+    }
+
+    @Override
+    public void open() {
+        oauth.openLoginForm();
+        assertCurrent();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index d54924b..e47deac 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -32,8 +32,10 @@ import org.apache.http.message.BasicNameValuePair;
 import org.junit.Assert;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.RSATokenVerifier;
+import org.keycloak.adapters.HttpClientBuilder;
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.common.VerificationException;
+import org.keycloak.common.util.KeystoreUtil;
 import org.keycloak.common.util.PemUtils;
 import org.keycloak.constants.AdapterConstants;
 import org.keycloak.jose.jwk.JSONWebKeySet;
@@ -61,12 +63,9 @@ import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.Charset;
+import java.security.KeyStore;
 import java.security.PublicKey;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -76,6 +75,7 @@ public class OAuthClient {
     public static final String SERVER_ROOT = AuthServerTestEnricher.getAuthServerContextRoot();
     public static final String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
     public static final String APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app";
+    private static final boolean sslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
 
     private Keycloak adminClient;
 
@@ -192,8 +192,38 @@ public class OAuthClient {
         fillLoginForm(username, password);
     }
 
+    private static CloseableHttpClient newCloseableHttpClient() {
+        if (sslRequired) {
+            KeyStore keystore = null;
+            // load the keystore containing the client certificate - keystore type is probably jks or pkcs12
+            String keyStorePath = System.getProperty("client.certificate.keystore");
+            String keyStorePassword = System.getProperty("client.certificate.keystore.passphrase");
+            try {
+                keystore = KeystoreUtil.loadKeyStore(keyStorePath, keyStorePassword);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            // load the trustore
+            KeyStore truststore = null;
+            String trustStorePath = System.getProperty("client.truststore");
+            String trustStorePassword = System.getProperty("client.truststore.passphrase");
+            try {
+                truststore = KeystoreUtil.loadKeyStore(trustStorePath, trustStorePassword);
+            } catch(Exception e) {
+                e.printStackTrace();
+            }
+            return (DefaultHttpClient)new HttpClientBuilder()
+                    .keyStore(keystore, keyStorePassword)
+                    .trustStore(truststore)
+                    .hostnameVerification(HttpClientBuilder.HostnameVerificationPolicy.ANY)
+                    .build();
+        }
+        return new DefaultHttpClient();
+    }
+
     public AccessTokenResponse doAccessTokenRequest(String code, String password) {
-        CloseableHttpClient client = new DefaultHttpClient();
+        CloseableHttpClient client = newCloseableHttpClient();
         try {
             HttpPost post = new HttpPost(getAccessTokenUrl());
 
@@ -296,7 +326,7 @@ public class OAuthClient {
 
     public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp,
                                                          String clientId, String clientSecret) throws Exception {
-        CloseableHttpClient client = new DefaultHttpClient();
+        CloseableHttpClient client = newCloseableHttpClient();
         try {
             HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index fc8b25b..5a50b08 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -142,6 +142,10 @@ public class ProvidersTest extends AbstractAuthenticationTest {
         addProviderInfo(result, "auth-spnego", "Kerberos", "Initiates the SPNEGO protocol.  Most often used with Kerberos.");
         addProviderInfo(result, "auth-username-password-form", "Username Password Form",
                 "Validates a username and password from login form.");
+        addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
+                "Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
+        addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
+                "Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
         addProviderInfo(result, "direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request");
         addProviderInfo(result, "direct-grant-validate-password", "Password",
                 "Validates the password supplied as a 'password' form parameter in direct grant request");
@@ -171,6 +175,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
                 "Testsuite Dummy authenticator.  Just passes through and is hardcoded to a specific user");
         addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru",
                 "Testsuite Dummy authenticator.  Just passes through and is hardcoded to a specific user");
+
         return result;
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java
new file mode 100644
index 0000000..2422bde
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.testsuite.x509;
+
+import org.jboss.logging.Logger;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.keycloak.admin.client.resource.AuthenticationManagementResource;
+import org.keycloak.authentication.AuthenticationFlow;
+import org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory;
+import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel;
+import org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory;
+import org.keycloak.common.util.Encode;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.events.admin.ResourceType;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
+import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.util.AdminEventPaths;
+import org.keycloak.testsuite.util.AssertAdminEvents;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USERNAME_EMAIL;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USER_ATTRIBUTE;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN_CN;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_CN;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 10/28/2016
+ */
+
+public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKeycloakTest {
+
+    public static final String EMPTY_CRL_PATH = "empty.crl";
+    public static final String CLIENT_CRL_PATH = "intermediate-ca.crl";
+    protected final Logger log = Logger.getLogger(this.getClass());
+
+    static final String REQUIRED = "REQUIRED";
+    static final String OPTIONAL = "OPTIONAL";
+    static final String DISABLED = "DISABLED";
+    static final String ALTERNATIVE = "ALTERNATIVE";
+
+    // TODO move to a base class
+    public static final String REALM_NAME = "test";
+
+    protected String userId;
+
+    protected String userId2;
+
+    protected AuthenticationManagementResource authMgmtResource;
+
+    protected AuthenticationExecutionInfoRepresentation browserExecution;
+
+    protected AuthenticationExecutionInfoRepresentation directGrantExecution;
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Rule
+    public AssertAdminEvents assertAdminEvents = new AssertAdminEvents(this);
+
+    protected boolean isImportAfterEachMethod() {
+        return true;
+    }
+
+    @Before
+    public void configureFlows() {
+        authMgmtResource = adminClient.realms().realm(REALM_NAME).flows();
+
+        AuthenticationFlowRepresentation browserFlow = copyBrowserFlow();
+        Assert.assertNotNull(browserFlow);
+
+        AuthenticationFlowRepresentation directGrantFlow = createDirectGrantFlow();
+        Assert.assertNotNull(directGrantFlow);
+
+        setBrowserFlow(browserFlow);
+        Assert.assertEquals(testRealm().toRepresentation().getBrowserFlow(), browserFlow.getAlias());
+
+        setDirectGrantFlow(directGrantFlow);
+        Assert.assertEquals(testRealm().toRepresentation().getDirectGrantFlow(), directGrantFlow.getAlias());
+        Assert.assertEquals(0, directGrantFlow.getAuthenticationExecutions().size());
+
+        // Add X509 cert authenticator to the direct grant flow
+        directGrantExecution = addAssertExecution(directGrantFlow, ValidateX509CertificateUsernameFactory.PROVIDER_ID, REQUIRED);
+        Assert.assertNotNull(directGrantExecution);
+
+        directGrantFlow = authMgmtResource.getFlow(directGrantFlow.getId());
+        Assert.assertNotNull(directGrantFlow.getAuthenticationExecutions());
+        Assert.assertEquals(1, directGrantFlow.getAuthenticationExecutions().size());
+
+        // Add X509 authenticator to the browser flow
+        browserExecution = addAssertExecution(browserFlow, X509ClientCertificateAuthenticatorFactory.PROVIDER_ID, ALTERNATIVE);
+        Assert.assertNotNull(browserExecution);
+
+        // Raise the priority of the authenticator to position it right before
+        // the Username/password authentication
+        // TODO find a better, more explicit way to specify the position
+        // of authenticator within the flow relative to other authenticators
+        authMgmtResource.raisePriority(browserExecution.getId());
+        // TODO raising the priority didn't generate the event?
+        //assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authRaiseExecutionPath(exec.getId()));
+
+        UserRepresentation user = findUser("test-user@localhost");
+        userId = user.getId();
+
+        user.singleAttribute("x509_certificate_identity","-");
+        updateUser(user);
+    }
+
+    private AuthenticationExecutionInfoRepresentation addAssertExecution(AuthenticationFlowRepresentation flow, String providerId, String requirement) {
+        AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation();
+        rep.setPriority(10);
+        rep.setAuthenticator(providerId);
+        rep.setRequirement(requirement);
+        rep.setParentFlow(flow.getId());
+
+        Response response = authMgmtResource.addExecution(rep);
+        // TODO the following statement asserts, the actual value is null?
+        //assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AssertAdminEvents.isExpectedPrefixFollowedByUuid(AdminEventPaths.authMgmtBasePath() + "/executions"), rep);
+        try {
+            Assert.assertEquals("added execution", 201, response.getStatus());
+        } finally {
+            response.close();
+        }
+        List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions(flow.getAlias());
+        return findExecution(providerId, executionReps);
+    }
+
+    AuthenticationExecutionInfoRepresentation findExecution(String providerId, List<AuthenticationExecutionInfoRepresentation> reps) {
+        for (AuthenticationExecutionInfoRepresentation exec : reps) {
+            if (providerId.equals(exec.getProviderId())) {
+                return exec;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+
+        ClientRepresentation app = ClientBuilder.create()
+                .id(KeycloakModelUtils.generateId())
+                .clientId("resource-owner")
+                .directAccessGrants()
+                .secret("secret")
+                .build();
+
+        UserRepresentation user = UserBuilder.create()
+                .id(KeycloakModelUtils.generateId())
+                .username("Keycloak")
+                .email("localhost@localhost")
+                .enabled(true)
+                .password("password")
+                .build();
+
+        userId2 = user.getId();
+
+        ClientRepresentation client = findTestApp(testRealm);
+        URI baseUri = URI.create(client.getRedirectUris().get(0));
+        URI redir = URI.create("https://localhost:" + System.getProperty("app.server.https.port", "8543") + baseUri.getRawPath());
+        client.getRedirectUris().add(redir.toString());
+
+        testRealm.setBruteForceProtected(true);
+        testRealm.setFailureFactor(2);
+
+        RealmBuilder.edit(testRealm)
+                .user(user)
+                .client(app);
+    }
+
+    AuthenticationFlowRepresentation createFlow(AuthenticationFlowRepresentation flowRep) {
+        Response response = authMgmtResource.createFlow(flowRep);
+        try {
+            org.keycloak.testsuite.Assert.assertEquals(201, response.getStatus());
+        }
+        finally {
+            response.close();
+        }
+        assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AssertAdminEvents.isExpectedPrefixFollowedByUuid(AdminEventPaths.authFlowsPath()), flowRep, ResourceType.AUTH_FLOW);
+
+        for (AuthenticationFlowRepresentation flow : authMgmtResource.getFlows()) {
+            if (flow.getAlias().equalsIgnoreCase(flowRep.getAlias())) {
+                return flow;
+            }
+        }
+        return null;
+    }
+
+    AuthenticationFlowRepresentation copyFlow(String existingFlow, String newFlow) {
+        // copy that should succeed
+        HashMap<String, String> params = new HashMap<>();
+        params.put("newName", newFlow);
+        Response response = authMgmtResource.copy(existingFlow, params);
+        assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, Encode.decode(AdminEventPaths.authCopyFlowPath(existingFlow)), params, ResourceType.AUTH_FLOW);
+        try {
+            Assert.assertEquals("Copy flow", 201, response.getStatus());
+        } finally {
+            response.close();
+        }
+        for (AuthenticationFlowRepresentation flow : authMgmtResource.getFlows()) {
+            if (flow.getAlias().equalsIgnoreCase(newFlow)) {
+                return flow;
+            }
+        }
+        return null;
+    }
+
+    AuthenticationFlowRepresentation createDirectGrantFlow() {
+        AuthenticationFlowRepresentation newFlow = newFlow("Copy-of-direct-grant", "desc", AuthenticationFlow.BASIC_FLOW, true, false);
+        return createFlow(newFlow);
+    }
+
+    AuthenticationFlowRepresentation newFlow(String alias, String description,
+                                             String providerId, boolean topLevel, boolean builtIn) {
+        AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation();
+        flow.setAlias(alias);
+        flow.setDescription(description);
+        flow.setProviderId(providerId);
+        flow.setTopLevel(topLevel);
+        flow.setBuiltIn(builtIn);
+        return flow;
+    }
+
+    AuthenticationFlowRepresentation copyBrowserFlow() {
+
+        RealmRepresentation realm = testRealm().toRepresentation();
+        return copyFlow(realm.getBrowserFlow(), "Copy-of-browser");
+    }
+
+    void setBrowserFlow(AuthenticationFlowRepresentation flow) {
+        RealmRepresentation realm = testRealm().toRepresentation();
+        realm.setBrowserFlow(flow.getAlias());
+        testRealm().update(realm);
+    }
+
+    void setDirectGrantFlow(AuthenticationFlowRepresentation flow) {
+        RealmRepresentation realm = testRealm().toRepresentation();
+        realm.setDirectGrantFlow(flow.getAlias());
+        testRealm().update(realm);
+    }
+
+    static AuthenticatorConfigRepresentation newConfig(String alias, Map<String,String> params) {
+        AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation();
+        config.setAlias(alias);
+        config.setConfig(params);
+        return config;
+    }
+
+    protected String createConfig(String executionId, AuthenticatorConfigRepresentation cfg) {
+        Response resp = authMgmtResource.newExecutionConfig(executionId, cfg);
+        try {
+            Assert.assertEquals(201, resp.getStatus());
+        }
+        finally {
+            resp.close();
+        }
+        return ApiUtil.getCreatedId(resp);
+    }
+
+    protected static X509AuthenticatorConfigModel createLoginSubjectEmail2UsernameOrEmailConfig() {
+        return new X509AuthenticatorConfigModel()
+                .setConfirmationPageAllowed(true)
+                .setMappingSourceType(SUBJECTDN_EMAIL)
+                .setUserIdentityMapperType(USERNAME_EMAIL);
+    }
+
+    protected static X509AuthenticatorConfigModel createLoginSubjectCN2UsernameOrEmailConfig() {
+        return new X509AuthenticatorConfigModel()
+                .setConfirmationPageAllowed(true)
+                .setMappingSourceType(SUBJECTDN_CN)
+                .setUserIdentityMapperType(USERNAME_EMAIL);
+    }
+
+    protected static X509AuthenticatorConfigModel createLoginIssuerCNToUsernameOrEmailConfig() {
+        return new X509AuthenticatorConfigModel()
+                .setConfirmationPageAllowed(true)
+                .setMappingSourceType(ISSUERDN_CN)
+                .setUserIdentityMapperType(USERNAME_EMAIL);
+    }
+
+    protected static X509AuthenticatorConfigModel createLoginIssuerDN_OU2CustomAttributeConfig() {
+        return new X509AuthenticatorConfigModel()
+                .setConfirmationPageAllowed(true)
+                .setMappingSourceType(ISSUERDN)
+                .setRegularExpression("O=(.*?)(?:,|$)")
+                .setUserIdentityMapperType(USER_ATTRIBUTE)
+                .setCustomAttributeName("x509_certificate_identity");
+    }
+
+    protected void setUserEnabled(String userName, boolean enabled) {
+        UserRepresentation user = findUser(userName);
+        Assert.assertNotNull(user);
+
+        user.setEnabled(enabled);
+
+        updateUser(user);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/OcspHandler.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/OcspHandler.java
new file mode 100644
index 0000000..4f66429
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/OcspHandler.java
@@ -0,0 +1,153 @@
+/*
+ * 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.testsuite.x509;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.Date;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
+import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.CRLReason;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.ocsp.BasicOCSPRespBuilder;
+import org.bouncycastle.cert.ocsp.CertificateID;
+import org.bouncycastle.cert.ocsp.CertificateStatus;
+import org.bouncycastle.cert.ocsp.OCSPReq;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
+import org.bouncycastle.cert.ocsp.Req;
+import org.bouncycastle.cert.ocsp.RespID;
+import org.bouncycastle.cert.ocsp.RevokedStatus;
+import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
+import org.bouncycastle.crypto.util.PrivateKeyFactory;
+import org.bouncycastle.crypto.util.PublicKeyFactory;
+import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.DigestCalculator;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
+
+import io.undertow.io.Sender;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.HeaderMap;
+import io.undertow.util.Headers;
+
+final class OcspHandler implements HttpHandler {
+
+    private static final String OCSP_RESPONDER_CERT_PATH = "/client-auth-test/intermediate-ca.crt";
+
+    private static final String OCSP_RESPONDER_KEYPAIR_PATH = "/client-auth-test/intermediate-ca.key";
+
+    // add any certificates that the OCSP responder needs to know about in the tests here
+    private static final Map<BigInteger, CertificateStatus> REVOKED_CERTIFICATES_STATUS = ImmutableMap
+            .of(BigInteger.valueOf(4096), new RevokedStatus(new Date(1472169600000L), CRLReason.unspecified));
+
+    private final SubjectPublicKeyInfo subjectPublicKeyInfo;
+
+    private final X509CertificateHolder[] chain;
+
+    private final AsymmetricKeyParameter privateKey;
+
+    OcspHandler() throws OperatorCreationException, GeneralSecurityException, IOException {
+        final Certificate certificate = CertificateFactory.getInstance("X509")
+                .generateCertificate(X509OCSPResponderTest.class.getResourceAsStream(OCSP_RESPONDER_CERT_PATH));
+
+        chain = new X509CertificateHolder[] {new X509CertificateHolder(certificate.getEncoded())};
+
+        final AsymmetricKeyParameter publicKey = PublicKeyFactory.createKey(certificate.getPublicKey().getEncoded());
+
+        subjectPublicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(publicKey);
+
+        final InputStream keyPairStream = X509OCSPResponderTest.class.getResourceAsStream(OCSP_RESPONDER_KEYPAIR_PATH);
+
+        try (final PEMParser keyPairReader = new PEMParser(new InputStreamReader(keyPairStream))) {
+            final PEMKeyPair keyPairPem = (PEMKeyPair) keyPairReader.readObject();
+            privateKey = PrivateKeyFactory.createKey(keyPairPem.getPrivateKeyInfo());
+        }
+    }
+
+    @Override
+    public void handleRequest(final HttpServerExchange exchange) throws Exception {
+        if (exchange.isInIoThread()) {
+            exchange.dispatch(this);
+            return;
+        }
+
+        final byte[] buffy = new byte[16384];
+        try (InputStream requestStream = exchange.getInputStream()) {
+            requestStream.read(buffy);
+        }
+
+        final OCSPReq request = new OCSPReq(buffy);
+        final Req[] requested = request.getRequestList();
+
+        final Extension nonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
+
+        final DigestCalculator sha1Calculator = new JcaDigestCalculatorProviderBuilder().build()
+                .get(AlgorithmIdentifier.getInstance(RespID.HASH_SHA1));
+
+        final BasicOCSPRespBuilder responseBuilder = new BasicOCSPRespBuilder(subjectPublicKeyInfo, sha1Calculator);
+
+        if (nonce != null) {
+            responseBuilder.setResponseExtensions(new Extensions(nonce));
+        }
+
+        for (final Req req : requested) {
+            final CertificateID certId = req.getCertID();
+
+            final BigInteger certificateSerialNumber = certId.getSerialNumber();
+            responseBuilder.addResponse(certId, REVOKED_CERTIFICATES_STATUS.get(certificateSerialNumber));
+        }
+
+        final ContentSigner contentSigner = new BcRSAContentSignerBuilder(
+                new AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption),
+                new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256)).build(privateKey);
+
+        final OCSPResp response = new OCSPRespBuilder().build(OCSPResp.SUCCESSFUL,
+                responseBuilder.build(contentSigner, chain, new Date()));
+
+        final byte[] responseBytes = response.getEncoded();
+
+        final HeaderMap responseHeaders = exchange.getResponseHeaders();
+        responseHeaders.put(Headers.CONTENT_TYPE, "application/ocsp-response");
+
+        final Sender responseSender = exchange.getResponseSender();
+        responseSender.send(ByteBuffer.wrap(responseBytes));
+
+        exchange.endExchange();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginTest.java
new file mode 100644
index 0000000..d5a69f1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509BrowserLoginTest.java
@@ -0,0 +1,434 @@
+/*
+ * 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.testsuite.x509;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel;
+import org.keycloak.events.Details;
+import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.x509.X509IdentityConfirmationPage;
+
+import javax.ws.rs.core.Response;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USERNAME_EMAIL;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USER_ATTRIBUTE;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @date 8/12/2016
+ */
+
+public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
+
+    @Page
+    protected AppPage appPage;
+
+    @Page
+    protected X509IdentityConfirmationPage loginConfirmationPage;
+
+    @Page
+    protected LoginPage loginPage;
+
+    private void login(X509AuthenticatorConfigModel config, String userId, String username, String attemptedUsername) {
+
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        loginConfirmationPage.open();
+
+        Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().startsWith("EMAILADDRESS=test-user@localhost"));
+        Assert.assertEquals(username, loginConfirmationPage.getUsernameText());
+        Assert.assertTrue(loginConfirmationPage.getLoginDelayCounterText().startsWith("The form will be submitted"));
+
+        loginConfirmationPage.confirm();
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+         events.expectLogin()
+                 .user(userId)
+                 .detail(Details.USERNAME, attemptedUsername)
+                 .removeDetail(Details.REDIRECT_URI)
+                 .assertEvent();
+    }
+
+    @Test
+    public void loginAsUserFromCertSubjectEmail() throws Exception {
+        // Login using an e-mail extracted from certificate's subject DN
+        login(createLoginSubjectEmail2UsernameOrEmailConfig(), userId, "test-user@localhost", "test-user@localhost");
+    }
+
+    @Test
+    public void loginIgnoreX509IdentityContinueToFormLogin() throws Exception {
+        // Set the X509 authenticator configuration
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        loginConfirmationPage.open();
+
+        Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().startsWith("EMAILADDRESS=test-user@localhost"));
+        Assert.assertEquals("test-user@localhost", loginConfirmationPage.getUsernameText());
+        Assert.assertTrue(loginConfirmationPage.getLoginDelayCounterText().startsWith("The form will be submitted"));
+
+        loginConfirmationPage.ignore();
+        loginPage.login("test-user@localhost", "password");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+         events.expectLogin()
+                 .user(userId)
+                 .detail(Details.USERNAME, "test-user@localhost")
+                 .removeDetail(Details.REDIRECT_URI)
+                 .assertEvent();
+    }
+
+    @Test
+    public void loginAsUserFromCertSubjectCN() {
+        // Login using a CN extracted from certificate's subject DN
+        login(createLoginSubjectCN2UsernameOrEmailConfig(), userId, "test-user@localhost", "test-user@localhost");
+    }
+
+    @Test
+    public void loginAsUserFromCertIssuerCN() {
+        login(createLoginIssuerCNToUsernameOrEmailConfig(), userId2, "keycloak", "Keycloak");
+    }
+
+    @Test
+    public void loginAsUserFromCertIssuerCNMappedToUserAttribute() {
+
+        UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
+        Assert.assertNotNull(user);
+
+        user.singleAttribute("x509_certificate_identity", "Red Hat");
+        this.updateUser(user);
+
+        events.clear();
+
+        login(createLoginIssuerDN_OU2CustomAttributeConfig(), userId2, "keycloak", "Red Hat");
+    }
+
+    @Test
+    public void loginDuplicateUsersNotAllowed() {
+
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginIssuerDN_OU2CustomAttributeConfig().getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        // Set up the users so that the identity extracted from X509 client cert
+        // matches more than a single user to trigger DuplicateModelException.
+
+        UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
+        Assert.assertNotNull(user);
+
+        user.singleAttribute("x509_certificate_identity", "Red Hat");
+        this.updateUser(user);
+
+        user = testRealm().users().get(userId).toRepresentation();
+        Assert.assertNotNull(user);
+
+        user.singleAttribute("x509_certificate_identity", "Red Hat");
+        this.updateUser(user);
+
+        events.clear();
+
+        loginPage.open();
+
+        Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed."));
+
+        loginPage.login("test-user@localhost", "password");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+        events.expectLogin()
+                .user(userId)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+    }
+
+    @Test
+    public void loginAttemptedNoConfig() {
+
+        loginConfirmationPage.open();
+        loginPage.assertCurrent();
+
+        Assert.assertThat(loginPage.getInfoMessage(), containsString("X509 client authentication has not been configured yet"));
+        // Continue with form based login
+        loginPage.login("test-user@localhost", "password");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+        events.expectLogin()
+                .user(userId)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+    }
+
+    @Test
+    public void loginWithX509CertCustomAttributeUserNotFound() {
+        X509AuthenticatorConfigModel config =
+                new X509AuthenticatorConfigModel()
+                        .setConfirmationPageAllowed(true)
+                        .setMappingSourceType(SUBJECTDN)
+                        .setRegularExpression("O=(.*?)(?:,|$)")
+                        .setCustomAttributeName("x509_certificate_identity")
+                        .setUserIdentityMapperType(USER_ATTRIBUTE);
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        loginConfirmationPage.open();
+        loginPage.assertCurrent();
+
+        // Verify there is an error message
+        Assert.assertNotNull(loginPage.getError());
+
+        Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed."));
+        events.expectLogin()
+                .user((String) null)
+                .session((String) null)
+                .error("user_not_found")
+                .detail(Details.USERNAME, "Red Hat")
+                .removeDetail(Details.CONSENT)
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+
+        // Continue with form based login
+        loginPage.login("test-user@localhost", "password");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+        events.expectLogin()
+                .user(userId)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+    }
+
+    @Test
+    public void loginWithX509CertCustomAttributeSuccess() {
+        X509AuthenticatorConfigModel config =
+                new X509AuthenticatorConfigModel()
+                        .setConfirmationPageAllowed(true)
+                        .setMappingSourceType(SUBJECTDN)
+                        .setRegularExpression("O=(.*?)(?:,|$)")
+                        .setCustomAttributeName("x509_certificate_identity")
+                        .setUserIdentityMapperType(USER_ATTRIBUTE);
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        // Update the attribute used to match the user identity to that
+        // extracted from the client certificate
+        UserRepresentation user = findUser("test-user@localhost");
+        Assert.assertNotNull(user);
+        user.singleAttribute("x509_certificate_identity", "Red Hat");
+        this.updateUser(user);
+
+        events.clear();
+
+        loginConfirmationPage.open();
+
+        Assert.assertTrue(loginConfirmationPage.getSubjectDistinguishedNameText().startsWith("EMAILADDRESS=test-user@localhost"));
+        Assert.assertEquals("test-user@localhost", loginConfirmationPage.getUsernameText());
+        Assert.assertTrue(loginConfirmationPage.getLoginDelayCounterText().startsWith("The form will be submitted"));
+
+        loginConfirmationPage.confirm();
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+    }
+
+    @Test
+    public void loginWithX509CertBadUserOrNotFound() {
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        // Delete user
+        UserRepresentation user = findUser("test-user@localhost");
+        Assert.assertNotNull(user);
+
+        Response response = testRealm().users().delete(userId);
+        assertEquals(204, response.getStatus());
+        response.close();
+        // TODO causes the test to fail
+        //assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.userResourcePath(userId));
+
+        loginConfirmationPage.open();
+        loginPage.assertCurrent();
+
+        // Verify there is an error message
+        Assert.assertNotNull(loginPage.getError());
+
+        Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed."));
+
+        events.expectLogin()
+                .user((String) null)
+                .session((String) null)
+                .error("user_not_found")
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.CONSENT)
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+
+        // Continue with form based login
+        loginPage.login("test-user@localhost", "password");
+        loginPage.assertCurrent();
+
+        Assert.assertEquals("test-user@localhost", loginPage.getUsername());
+        Assert.assertEquals("", loginPage.getPassword());
+
+        Assert.assertEquals("Invalid username or password.", loginPage.getError());
+    }
+
+    @Test
+    public void loginValidCertificateDisabledUser() {
+        setUserEnabled("test-user@localhost", false);
+
+        try {
+            AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig());
+            String cfgId = createConfig(browserExecution.getId(), cfg);
+            Assert.assertNotNull(cfgId);
+
+            loginConfirmationPage.open();
+            loginPage.assertCurrent();
+
+            Assert.assertNotNull(loginPage.getError());
+
+            Assert.assertThat(loginPage.getError(), containsString("X509 certificate authentication's failed.\nUser is disabled"));
+
+            events.expectLogin()
+                    .user(userId)
+                    .session((String) null)
+                    .error("user_disabled")
+                    .detail(Details.USERNAME, "test-user@localhost")
+                    .removeDetail(Details.CONSENT)
+                    .removeDetail(Details.REDIRECT_URI)
+                    .assertEvent();
+
+            loginPage.login("test-user@localhost", "password");
+            loginPage.assertCurrent();
+
+            // KEYCLOAK-1741 - assert form field values kept
+            Assert.assertEquals("test-user@localhost", loginPage.getUsername());
+            Assert.assertEquals("", loginPage.getPassword());
+
+            // KEYCLOAK-2024
+            Assert.assertEquals("Account is disabled, contact admin.", loginPage.getError());
+
+            events.expectLogin()
+                    .user(userId)
+                    .session((String) null)
+                    .error("user_disabled")
+                    .detail(Details.USERNAME, "test-user@localhost")
+                    .removeDetail(Details.CONSENT)
+                    .removeDetail(Details.REDIRECT_URI)
+                    .assertEvent();
+        } finally {
+            setUserEnabled("test-user@localhost", true);
+        }
+    }
+
+    @Test
+    public void loginWithX509WithEmptyRevocationList() {
+        X509AuthenticatorConfigModel config =
+                new X509AuthenticatorConfigModel()
+                        .setCRLEnabled(true)
+                        .setCRLRelativePath(EMPTY_CRL_PATH)
+                        .setConfirmationPageAllowed(true)
+                        .setMappingSourceType(SUBJECTDN_EMAIL)
+                        .setUserIdentityMapperType(USERNAME_EMAIL);
+        login(config, userId, "test-user@localhost", "test-user@localhost");
+    }
+
+    @Test
+    public void loginCertificateRevoked() {
+        X509AuthenticatorConfigModel config =
+                new X509AuthenticatorConfigModel()
+                        .setCRLEnabled(true)
+                        .setCRLRelativePath(CLIENT_CRL_PATH)
+                        .setConfirmationPageAllowed(true)
+                        .setMappingSourceType(SUBJECTDN_EMAIL)
+                        .setUserIdentityMapperType(USERNAME_EMAIL);
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        loginConfirmationPage.open();
+        loginPage.assertCurrent();
+
+        // Verify there is an error message
+        Assert.assertNotNull(loginPage.getError());
+
+        Assert.assertThat(loginPage.getError(), containsString("Certificate validation's failed.\nCertificate has been revoked, certificate's subject:"));
+
+        // Continue with form based login
+        loginPage.login("test-user@localhost", "password");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+        events.expectLogin()
+                .user(userId)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+    }
+
+    @Test
+    public void loginNoIdentityConfirmationPage() {
+        X509AuthenticatorConfigModel config =
+                new X509AuthenticatorConfigModel()
+                    .setConfirmationPageAllowed(false)
+                    .setMappingSourceType(SUBJECTDN_EMAIL)
+                    .setUserIdentityMapperType(USERNAME_EMAIL);
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", config.getConfig());
+        String cfgId = createConfig(browserExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        oauth.openLoginForm();
+        // X509 authenticator extracts the user identity, maps it to an existing
+        // user and automatically logs the user in without prompting to confirm
+        // the identity.
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+        events.expectLogin()
+                .user(userId)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509DirectGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509DirectGrantTest.java
new file mode 100644
index 0000000..2582604
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509DirectGrantTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.testsuite.x509;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.util.OAuthClient;
+
+import javax.ws.rs.core.Response;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USERNAME_EMAIL;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USER_ATTRIBUTE;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
+
+/**
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 10/28/2016
+ */
+
+public class X509DirectGrantTest extends AbstractX509AuthenticationTest {
+
+    @Test
+    public void loginFailedOnDuplicateUsers() throws Exception {
+
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", createLoginIssuerDN_OU2CustomAttributeConfig().getConfig());
+        String cfgId = createConfig(directGrantExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        // Set up the users so that the identity extracted from X509 client cert
+        // matches more than a single user to trigger DuplicateModelException.
+
+        UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
+        Assert.assertNotNull(user);
+
+        user.singleAttribute("x509_certificate_identity", "Red Hat");
+        this.updateUser(user);
+
+        user = testRealm().users().get(userId).toRepresentation();
+        Assert.assertNotNull(user);
+
+        user.singleAttribute("x509_certificate_identity", "Red Hat");
+        this.updateUser(user);
+
+        events.clear();
+
+        oauth.clientId("resource-owner");
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
+
+        assertEquals(401, response.getStatusCode());
+        assertEquals("invalid_request", response.getError());
+        Assert.assertThat(response.getErrorDescription(), containsString("X509 certificate authentication's failed."));
+    }
+
+    @Test
+    public void loginFailedOnInvalidUser() throws Exception {
+
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", createLoginIssuerDN_OU2CustomAttributeConfig().getConfig());
+        String cfgId = createConfig(directGrantExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
+        Assert.assertNotNull(user);
+
+        user.singleAttribute("x509_certificate_identity", "-");
+        this.updateUser(user);
+
+        events.clear();
+
+        oauth.clientId("resource-owner");
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
+
+        events.expectLogin()
+                .user((String) null)
+                .session((String) null)
+                .error(Errors.INVALID_USER_CREDENTIALS)
+                .client("resource-owner")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.USERNAME)
+                .removeDetail(Details.CONSENT)
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+
+        assertEquals(401, response.getStatusCode());
+        assertEquals("invalid_grant", response.getError());
+        assertEquals("Invalid user credentials", response.getErrorDescription());
+    }
+
+    @Test
+    public void loginFailedDisabledUser() throws Exception {
+        setUserEnabled("test-user@localhost", false);
+
+        try {
+            AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig());
+            String cfgId = createConfig(directGrantExecution.getId(), cfg);
+            Assert.assertNotNull(cfgId);
+
+            oauth.clientId("resource-owner");
+            OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
+
+            events.expectLogin()
+                    .user(userId)
+                    .session((String) null)
+                    .error(Errors.USER_DISABLED)
+                    .client("resource-owner")
+                    .detail(Details.USERNAME, "test-user@localhost")
+                    .removeDetail(Details.CODE_ID)
+                    .removeDetail(Details.CONSENT)
+                    .removeDetail(Details.REDIRECT_URI)
+                    .assertEvent();
+
+            assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
+            assertEquals("invalid_grant", response.getError());
+            assertEquals("Account disabled", response.getErrorDescription());
+
+        } finally {
+            setUserEnabled("test-user@localhost", true);
+        }
+    }
+
+    private void loginForceTemporaryAccountLock() throws Exception {
+        X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel()
+                .setMappingSourceType(ISSUERDN)
+                .setRegularExpression("OU=(.*?)(?:,|$)")
+                .setUserIdentityMapperType(USER_ATTRIBUTE)
+                .setCustomAttributeName("x509_certificate_identity");
+
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", config.getConfig());
+        String cfgId = createConfig(directGrantExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        UserRepresentation user = testRealm().users().get(userId).toRepresentation();
+        Assert.assertNotNull(user);
+
+        user.singleAttribute("x509_certificate_identity", "-");
+        this.updateUser(user);
+
+        events.clear();
+
+        oauth.clientId("resource-owner");
+        oauth.doGrantAccessTokenRequest("secret", "", "", null);
+        oauth.doGrantAccessTokenRequest("secret", "", "", null);
+        oauth.doGrantAccessTokenRequest("secret", "", "", null);
+
+        events.clear();
+    }
+
+
+    @Test
+    @Ignore
+    public void loginFailedTemporarilyDisabledUser() throws Exception {
+
+        loginForceTemporaryAccountLock();
+
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig());
+        String cfgId = createConfig(directGrantExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        oauth.clientId("resource-owner");
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
+
+        events.expectLogin()
+                .user(userId)
+                .session((String) null)
+                .error(Errors.USER_TEMPORARILY_DISABLED)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.CONSENT)
+                .removeDetail(Details.REDIRECT_URI)
+                .assertEvent();
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
+        assertEquals("invalid_grant", response.getError());
+        assertEquals("Account temporarily disabled", response.getErrorDescription());
+    }
+
+
+    private void doResourceOwnerCredentialsLogin(String clientId, String clientSecret, String login, String password) throws Exception {
+
+        oauth.clientId(clientId);
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(clientSecret, "", "", null);
+
+        assertEquals(200, response.getStatusCode());
+
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectLogin()
+                .client(clientId)
+                .user(userId)
+                .session(accessToken.getSessionState())
+                .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.USERNAME, login)
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+    }
+
+    @Test
+    public void loginResourceOwnerCredentialsSuccess() throws Exception {
+        X509AuthenticatorConfigModel config =
+                new X509AuthenticatorConfigModel()
+                        .setMappingSourceType(SUBJECTDN_EMAIL)
+                        .setUserIdentityMapperType(USERNAME_EMAIL);
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", config.getConfig());
+        String cfgId = createConfig(directGrantExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        doResourceOwnerCredentialsLogin("resource-owner", "secret", "test-user@localhost", "");
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java
new file mode 100644
index 0000000..f44fc64
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509OCSPResponderTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.testsuite.x509;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel;
+import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
+import org.keycloak.testsuite.util.OAuthClient;
+
+import javax.ws.rs.core.Response;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USERNAME_EMAIL;
+import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
+
+import io.undertow.Undertow;
+import io.undertow.server.handlers.BlockingHandler;
+
+/**
+ * Verifies Certificate revocation using OCSP responder.
+ * The tests rely on an OCSP responder service listening
+ * for OCSP requests on http://localhost:8888
+ * @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
+ * @version $Revision: 1 $
+ * @since 11/2/2016
+ */
+
+public class X509OCSPResponderTest extends AbstractX509AuthenticationTest {
+
+    private static final String OCSP_RESPONDER_HOST = "localhost";
+
+    private static final int OCSP_RESPONDER_PORT = 8888;
+
+    private Undertow ocspResponder;
+
+    @Test
+    public void loginFailedOnOCSPResponderRevocationCheck() throws Exception {
+        X509AuthenticatorConfigModel config =
+                new X509AuthenticatorConfigModel()
+                        .setOCSPEnabled(true)
+                        .setMappingSourceType(SUBJECTDN_EMAIL)
+                        .setUserIdentityMapperType(USERNAME_EMAIL);
+        AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", config.getConfig());
+        String cfgId = createConfig(directGrantExecution.getId(), cfg);
+        Assert.assertNotNull(cfgId);
+
+        oauth.clientId("resource-owner");
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
+
+        assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatusCode());
+        assertEquals("invalid_request", response.getError());
+
+        Assert.assertThat(response.getErrorDescription(), containsString("Certificate's been revoked."));
+    }
+
+    @Before
+    public void startOCSPResponder() throws Exception {
+        ocspResponder = Undertow.builder().addHttpListener(OCSP_RESPONDER_PORT, OCSP_RESPONDER_HOST)
+                .setHandler(new BlockingHandler(new OcspHandler())).build();
+
+        ocspResponder.start();
+    }
+
+    @After
+    public void stopOCSPResponder() {
+        ocspResponder.stop();
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index 3085ccc..e63edcd 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -26,7 +26,7 @@
         <property name="browser">${browser}</property>
         <property name="htmlUnit.version">${htmlUnitBrowserVersion}</property>
         <property name="firefox_binary">${firefox_binary}</property>
-        <property name="phantomjs.cli.args">${phantomjs.cli.args}</property>
+        <property name="phantomjs.cli.args">${phantomjs.cli.args} --ssl-certificates-path=${client.certificate.ca.path} --ssl-client-certificate-file=${client.certificate.file} --ssl-client-key-file=${client.key.file} --ssl-client-key-passphrase=${client.key.passphrase}</property>
     </extension>
     
     <extension qualifier="graphene">
@@ -173,4 +173,4 @@
         </configuration>
     </container>
             
-</arquillian>
\ No newline at end of file
+</arquillian>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/intermediate-ca.crt b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/intermediate-ca.crt
new file mode 100644
index 0000000..73ad33f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/intermediate-ca.crt
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgTAk1BMREwDwYDVQQHEwhXZXN0d29yZDEQMA4GA1UEChMHUmVkIEhh
+dDERMA8GA1UECxMIS2V5Y2xvYWsxEjAQBgNVBAMTCWxvY2FsaG9zdDAgFw0xNjEw
+MzEyMDA2NTJaGA8zMDE2MDMwMzIwMDY1MlowYzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xEDAOBgNVBAoMB1JlZCBIYXQxETAPBgNV
+BAsMCEtleWNsb2FrMREwDwYDVQQDDAhLZXljbG9hazCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAOyWSWjty1OtggI8zI+tbLy0mdyE5z3pnqUuspLZcrb3
+sWoSWh0m+OhZOH8wIldav3+nNtM3G5dLK8L+iMRFu/1clpIDtzoLSV/v0HxiovJi
+rOW9PhfyMu+vQE3D27zgOspW6leETQhO6tNKKT/NqBkgAIyYp0zAGT3tVs3k7mL+
+jVorVf8lbExOaomH3S2HoxFUHsMUBkS0WYV2tN1Fyqp9ieVGBu0fiItnZAOofi6G
+kS1L2ZBRbHW4eRbr77gUsETgPYXkPdN0mT7KC90KLEJfie7TyQf7O7EACQb+Y5nG
+ySBT93fDTaIbJ2JR4UYwLXiLGZF4k+mvlQj2PJAG1W0CAwEAAaN+MHwwHQYDVR0O
+BBYEFJ1G0xr/t9MQ/8lyXdmgbdoxAoOjMB8GA1UdIwQYMBaAFFCfEXmWKTtaiZG7
+tCvBrmQiujrLMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBYG
+A1UdJQEB/wQMMAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQAbcogxp/yb
+i7gF1Z+mZUwhqd2hqmMlE2AmLrZuo5jsq66XhwQTk13kvUYExlQw6DStrKDuRySj
+DRIQhu5UU4hLhewa9yl8iRs/zvZQsShJZ0vJPBhisTzU5vnA+ioHqTF91DX66xHE
+l57O8Vcmt8fCAg+LpWTokNvy04/0+vmy0od1/LF9sawTgXYu5g+o4JYzurOMxH9v
+Kn6cOLvpiAO/BsCYXKZxTu6WQX2N4AVRDVBJSiTvHylXtQX/t7fMIm27qU/TYona
+jWmZjy8Up99LpL+q6L4W8zZc35jzKF64Th6lMz/shc4/BHa1QVPgQ9yRWnoNJvUz
+9IceMNNiEnA5
+-----END CERTIFICATE-----
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/intermediate-ca.key b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/intermediate-ca.key
new file mode 100644
index 0000000..d1dc6cf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/intermediate-ca.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA7JZJaO3LU62CAjzMj61svLSZ3ITnPemepS6yktlytvexahJa
+HSb46Fk4fzAiV1q/f6c20zcbl0srwv6IxEW7/VyWkgO3OgtJX+/QfGKi8mKs5b0+
+F/Iy769ATcPbvOA6ylbqV4RNCE7q00opP82oGSAAjJinTMAZPe1WzeTuYv6NWitV
+/yVsTE5qiYfdLYejEVQewxQGRLRZhXa03UXKqn2J5UYG7R+Ii2dkA6h+LoaRLUvZ
+kFFsdbh5FuvvuBSwROA9heQ903SZPsoL3QosQl+J7tPJB/s7sQAJBv5jmcbJIFP3
+d8NNohsnYlHhRjAteIsZkXiT6a+VCPY8kAbVbQIDAQABAoIBAAPhdzTMacBmoTJO
+MwDMVHNH9xoh5/UhBuQovu8ft/z+VR+rS2UdBYoyJHYZoQaTy35ZjNGsHry9k+sv
+56hoiGvgf+vHOdMu6jYzpdTUfV3CdpmSIBmedG1wD3r3EnynpO86u79RwT24patv
+lPuh8PbwinHD80KUCBX29ayM68gd4rptebF+BW7iN1yvw4lgPLbCAgjRgRee2r7i
+kTDQKgqSV9oZObJB10Lf42E/COXzAj2gJo55bjQFoZtpaueaLZNn83hKNsEFOwYi
+Xe9I5PGs0xBltiWGuodmUMgat4BS25Bg398rVZhC8HC79NfLeD6XRMWS2muVM8eR
+/A61TGECgYEA+uDFw/cH/sYx32JGcTRHGdbwNDI9edIB5B/WP6D21pql7LZWPGa2
+uSNP1fDJwbqEGKxHLGOJYBHySao8Vnjl38ppwj2pPQAqnhQW7gy2Re/tl8y/2GF8
+EMFn3o7/Nb85NdydBlUU0Lf3MpI0C8GEQ0EA6KeRrb0H2q77/KAQNUUCgYEA8WrS
+XxPaDVaRLGWePjey0laOl7wNGpZuQ3MgUiN1FyGcdFs2VEr8GFhW1umSvDgFSHyj
+eN+gmBHkwWTSSqfxzukSfhUbFQLrqtOvhyeHC5MJOf+Z4hE2haLzVIT5y7sIe2Tz
+IbScrwUpRDDx4QIYvD3Y7cu/XpG3W90/oxOZfgkCgYEAsSJ9YGUOdxv3YLMh65Iu
+1ZbGWQRUFaxq+2hjPN/pCRN+Lgl8D/+x6jx9hSATRD7uQAF762KIP5XMTajuG1wX
+apCfZa9lzBVPAfhLTF8bX3wNdM3zctM55xa3wROULJ3MxjPzhuIR5WDPIBzMtLQ5
+L8c43gxatqIuZoCAmRlm+EUCgYEAnvgbbRYyaMQPNm/3wrqEmzL6FHp1GoPiObIO
+n2fPy5qLXqdNYDY8SH+rNzt5L02oVPIV5xtCscBepOaR/y0V8ozRa4rUWYKh0swv
+8kyaBYod8j4Yg5+YCmmh1470ui3/yNlRrGk1H57DbK7aJ7NsvE/xj6yx6EEFv40y
+QV0/npkCgYEAxqrBDuoWx71zghC33g+lE73G7x21M/565rc+UOMOgyCCBUvc74DE
+JXe6/hpPc4wF5yLosJ+pkzT1YlTTzwEiYwpwE73DxrZRmnPHPqUMQDyfHRg8HGs3
+hJQ/jdKyqtwxmEoCAcaq5p+sa9WuxrvLqfquvEkeL2xNAKrkq/Iw4Ug=
+-----END RSA PRIVATE KEY-----
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 53d7684..228bc9e 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -83,6 +83,16 @@
         <testsuite.constants>${project.build.directory}/dependency/test-constants.properties</testsuite.constants>
 
         <skip.add.user.json>false</skip.add.user.json>
+        <client.certificate.keystore>${auth.server.config.dir}/client.jks</client.certificate.keystore>
+        <client.certificate.keystore.passphrase>secret</client.certificate.keystore.passphrase>
+        <client.truststore>${auth.server.config.dir}/keycloak.truststore</client.truststore>
+        <client.truststore.passphrase>secret</client.truststore.passphrase>
+        <client.certificate.ca.path>${auth.server.config.dir}/ca.crt</client.certificate.ca.path>
+        <client.certificate.file>${auth.server.config.dir}/client.crt</client.certificate.file>
+        <client.key.file>${auth.server.config.dir}/client.key</client.key.file>
+        <client.key.passphrase>secret</client.key.passphrase>
+
+        <auth.server.ocsp.responder.enabled>false</auth.server.ocsp.responder.enabled>
     </properties>
 
     <build>
@@ -212,6 +222,17 @@
                             <project.version>${project.version}</project.version>
                             <migration.project.version>${migration.project.version}</migration.project.version>
                             <migration.product.version>${migration.product.version}</migration.product.version>
+
+                            <client.certificate.ca.path>${client.certificate.ca.path}</client.certificate.ca.path>
+                            <client.certificate.keystore>${client.certificate.keystore}</client.certificate.keystore>
+                            <client.certificate.keystore.passphrase>${client.certificate.keystore.passphrase}</client.certificate.keystore.passphrase>
+                            <client.truststore>${client.truststore}</client.truststore>
+                            <client.truststore.passphrase>${client.truststore.passphrase}</client.truststore.passphrase>
+                            <client.certificate.file>${client.certificate.file}</client.certificate.file>
+                            <client.key.file>${client.key.file}</client.key.file>
+                            <client.key.passphrase>${client.key.passphrase}</client.key.passphrase>
+
+                            <auth.server.ocsp.responder.enabled>${auth.server.ocsp.responder.enabled}</auth.server.ocsp.responder.enabled>
                         </systemPropertyVariables>
                         <properties>
                             <property>
diff --git a/themes/src/main/resources/theme/base/login/login-x509-info.ftl b/themes/src/main/resources/theme/base/login/login-x509-info.ftl
new file mode 100644
index 0000000..8bd0dfc
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/login-x509-info.ftl
@@ -0,0 +1,78 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+    <#if section = "title">
+        ${msg("loginTitle",(realm.displayName!''))}
+    <#elseif section = "header">
+        ${msg("loginTitleHtml",(realm.displayNameHtml!''))}
+    <#elseif section = "form">
+
+        <form id="kc-x509-login-info" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
+            <div class="${properties.kcFormGroupClass!}">
+
+                <div class="${properties.kcLabelWrapperClass!}">
+                    <label for="certificate_subjectDN" class="${properties.kcLabelClass!}">X509 client certificate: </label>
+                </div>
+                <#if subjectDN??>
+                    <div class="${properties.kcLabelWrapperClass!}">
+                         <label id="certificate_subjectDN" class="${properties.kcLabelClass!}">${(subjectDN!"")?html}</label>
+                    </div>
+                <#else>
+                    <div class="${properties.kcLabelWrapperClass!}">
+                        <label id="certificate_subjectDN" class="${properties.kcLabelClass!}">[No Certificate]</label>
+                    </div>
+                </#if>
+           </div>
+
+            <div class="${properties.kcFormGroupClass!}">
+
+                    <#if isUserEnabled>
+                          <div class="${properties.kcLabelWrapperClass!}">
+                             <label for="username" class="${properties.kcLabelClass!}">You will be logged in as:</label>
+                          </div>
+                          <div class="${properties.kcLabelWrapperClass!}">
+                             <label id="username" class="${properties.kcLabelClass!}">${(username!'')?html}</label>
+                         </div>
+                    </#if>
+
+            </div>
+
+            <div class="${properties.kcFormGroupClass!}">
+                <div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
+                    <div class="${properties.kcFormOptionsWrapperClass!}">
+                    </div>
+                </div>
+
+                <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
+                    <div class="${properties.kcFormButtonsWrapperClass!}">
+                        <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="Continue"/>
+                        <#if isUserEnabled>
+                            <input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="Ignore"/>
+                        </#if>
+                    </div>
+                </div>
+		<span id="counter">The form will be submitted in -- seconds</span>
+            </div>
+        </form>
+<script>
+
+var n = 10;
+function autoSubmitCountdown(){
+    var c=n;
+    setInterval(function(){
+        if(c>=0){
+    	     document.getElementById("counter").textContent = "The form will be submitted in " + c + " seconds";
+        }
+        if(c==0){
+	    document.forms[0].submit();
+        }
+        c--;
+    },1000);
+}
+
+// Start
+autoSubmitCountdown();
+
+</script>
+    </#if>
+
+</@layout.registrationLayout>