KEYCLOAK-4335: x509 client certificate authentication

Started on implementing cert thumbprint validation as a part of x509 auth flow. Added a prompt screen to give users a choice to either log in based on the identity extracted from X509 cert or to continue with normal browser login flow authentication; clean up some of the comments

x509 authentication for browser and direct grant flows. Implemented certificate to user mapping based on user attribute

Implemented CRL and OCSP certificate revocation checking and added corresponding configuration settings to set up responderURI (OCSP), a location of a file containing X509CRL entries and switiches to enable/disable revocation checking; reworked the certificate validation; removed superflous logging; changed the certificate authentication prompt page to automatically log in the user after 10 seconds if no response from user is received

Support for loading CRL from LDAP directory; finished the CRL checking using the distribution points in the certificate; updated the instructions how to add X509 authentication to keycloak authentication flows; minor styling changes

Stashing x509 unit test related changes; added the steps to configure mutual SSL in WildFly to the summary document

A minor fix to throw a security exception when unable to check cert revocation status using OCSP; continue working on README

Changes to the formating of the readme

Added a list of features to readme

Fixed a potential bug in X509 cert user authenticator that may cause NPE if the client certificate does not define keyusage or extended key usage extensions

Fixed compile time errors in X509 validators caused by the changes to the user credentials model in upstream master

Removed a superfluous file created when merging x509 and main branches

X509 authentication: removed the PKIX path validation as superflous

Reverted changes to the AbstractAttributeMapper introduced during merging of x509 branch into main

Merge the unit tests from x509 branch

added mockito dependency to services project; changes to the x509 authenticators to expose methods in order to support unit tests; added a default ctor to CertificateValidator class to support unit testing; updated the direct grant and browser x509 authenticators to report consistent status messages; unit tests to validate X509 direct grant and browser authenticators; fixed OCSP validation to throw an exception if the certificate chain contains a single certificate; fixed the CRL revocation validation to only use CRL distribution point validation only if configured

CRL and OSCP mock tests using mock netty server. Changed the certificate validator to better support unit testing.

changes to the mockserver dependency to explicitly exclude xercesImpl that was causing SAMLParsingTest to fail

Added a utility class to build v3 certificates with optional extensions to facilitate X509 unit testing; removed supoerfluous certificate date validity check (undertow should be checking the certificate dates during PKIX path validation anyway)

X509: changes to make configuring the user identity extraction simplier for users - new identity sources to map certificate CN and email (E) attributes from X500 subject and issuer names directly rather than using regular expressions to parse them

X509 fixed a compile error caused by the changes to the user model in master

Integration tests to validate X509 client certificate authentication

Minor tweaks to X509 client auth related integration tests

CRLs to support x509 client cert auth integration tests

X509: reverted the changes to testrealm.json and updated the test to configure the realm at runtime

X509 - changes to the testsuite project configuration to specify a path to a trust store used to test x509 direct grant flow; integration tests to validate x509 authentication in browser and direct grant flows; updated the client certificate to extend its validatity dates; x509 integration tests and authenticators have been refactored to use a common configuration class

X509 separated the browser and direct grant x509 authenction integration tests

x509 updated the authenticator provider test to remove no longer supported cert thumbprint authenticator

x509 removed the dependency on mockito

x509 re-implemented OCSP certificate revocation client used to check revocation status when logging in with x509 certificate to work around the dependency on Sun OCSP implementation; integration tests to verify OCSP revocation requests

index.txt.attr is needed by openssl to run a simple OCSP server

x509: minor grammar fixes

Add OCSP stub responder to integration tests

This commit adds OCSP stub responder needed for the integration tests,
and eliminates the need to run external OCSP responder in order to run
the OCSP in X509OCSPResponderTest.

Replace printStackTrece with logging

This commit replaces call to printStackTrace that will end up going to
the stderr with logging statement of WARN severity.

Remove unused imports

Removed unused imports in
org.keycloak.authentication.authenticators.x509 package.

Parameterized Hashtable variable

Removed unused CertificateFactory variable

Declared serialVersionUID for Serializable class

Removed unused CertificateBuilder class

The CertificateBuilder was not used anywhere in the code, removing it to
prevent technical debt.

Removing unused variable declaration

`response` variable is not used in the test, removed it.

Made sure InputStreams are closed

Even though the InputStreams are memory based, added try-with-resources
to make sure that they are closed.

Removed deprecated usage of URLEncoder

Replaced invocation of deprecated method from URLEncoder with Encode
from Keycloak util package.

Made it more clear how to control OCSP stub responder in the tests

X509 Certificate user authentication: moved the integration unit tests into their own directory to fix a failing travis test job

KEYCLOAK-4335: reduced the logging level; added the instructions how to run X.509 related tests to HOW-TO-RUN.md doc; removed README.md from x509 folder; removed no longer used ocsp profile and fixed the exclusion filter; refactored the x509 base test class that was broken by the recent changes to the integration tests

KEYCLOAK-4335: fixed a few issues after rebasing
This commit is contained in:
Peter Nalyvayko committed 2017-03-17 05:24:57 -04:00
1 parent 3551912506
commit b2f10359c8
47 files changed
+4678 -15

No files matched your search

@@ -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;
}
}
@@ -40,6 +40,15 @@ import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider; import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 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.math.BigInteger;
import java.security.KeyPair; import java.security.KeyPair;
@@ -75,7 +84,6 @@ public class CertificateUtils {
*/ */
public static X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert, public static X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert,
String subject) throws Exception { String subject) throws Exception {
try { try {
X500Name subjectDN = new X500Name("CN=" + subject); X500Name subjectDN = new X500Name("CN=" + subject);
@@ -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;
}
}
@@ -66,5 +66,6 @@
<module name="org.apache.httpcomponents"/> <module name="org.apache.httpcomponents"/>
<module name="org.twitter4j"/> <module name="org.twitter4j"/>
<module name="javax.transaction.api"/> <module name="javax.transaction.api"/>
<module name="sun.jdk"/>
</dependencies> </dependencies>
</module> </module>
@@ -27,6 +27,9 @@ import org.keycloak.admin.client.resource.ServerInfoResource;
import org.keycloak.admin.client.token.TokenManager; import org.keycloak.admin.client.token.TokenManager;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import java.net.URI; import java.net.URI;
import java.security.KeyStore; import java.security.KeyStore;
@@ -46,6 +49,7 @@ public class Keycloak {
private String authToken; private String authToken;
private final ResteasyWebTarget target; private final ResteasyWebTarget target;
private final ResteasyClient client; 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) { 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); 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); 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) { 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); return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null, null);
} }
@@ -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) {
}
}
@@ -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() {
}
}
@@ -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) {
}
}
@@ -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));
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -34,4 +34,6 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
+13 -1
View File
@@ -295,4 +295,16 @@ To run the tests run:
#### Internet Explorer #### Internet Explorer
* **Supported version:** 11 * **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) * **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` * **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.*"
@@ -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-----
@@ -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-----
@@ -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-----
@@ -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-----
@@ -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-----
@@ -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-----
@@ -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-----
@@ -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
@@ -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-----
@@ -39,6 +39,9 @@
<keystore path="keycloak.jks" relative-to="jboss.server.config.dir" keystore-password="secret"/> <keystore path="keycloak.jks" relative-to="jboss.server.config.dir" keystore-password="secret"/>
</ssl> </ssl>
</server-identities> </server-identities>
<authentication>
<truststore path="keycloak.truststore" relative-to="jboss.server.config.dir" keystore-password="secret"/>
</authentication>
</security-realm> </security-realm>
</xsl:copy> </xsl:copy>
</xsl:template> </xsl:template>
@@ -46,7 +49,7 @@
<http-listener name="default" socket-binding="http" redirect-socket="proxy-https" proxy-address-forwarding="true"/> <http-listener name="default" socket-binding="http" redirect-socket="proxy-https" proxy-address-forwarding="true"/>
</xsl:template> </xsl:template>
<xsl:template match="//u:host"> <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:copy-of select="."/>
</xsl:template> </xsl:template>
@@ -236,6 +236,12 @@
<includes> <includes>
<include>keycloak.jks</include> <include>keycloak.jks</include>
<include>keycloak.truststore</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> </includes>
</resource> </resource>
</resources> </resources>
@@ -343,6 +349,10 @@
<name>auth.server.ssl.required</name> <name>auth.server.ssl.required</name>
</property> </property>
</activation> </activation>
<properties>
<!--disable exclusion pattern for x509 tests, which is enabled by default in the base/pom.xml-->
<exclude.x509>-</exclude.x509>
</properties>
<build> <build>
<pluginManagement> <pluginManagement>
<plugins> <plugins>
@@ -388,6 +398,12 @@
<includes> <includes>
<include>keycloak.jks</include> <include>keycloak.jks</include>
<include>keycloak.truststore</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> </includes>
</resource> </resource>
</resources> </resources>
@@ -40,7 +40,8 @@
<exclude.client>-</exclude.client> <exclude.client>-</exclude.client>
<!--exclude cluster tests by default, enabled by 'auth-server-*-cluster' profiles in tests/pom.xml--> <!--exclude cluster tests by default, enabled by 'auth-server-*-cluster' profiles in tests/pom.xml-->
<exclude.cluster>**/cluster/**/*Test.java</exclude.cluster> <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 tests. They can be added by -Dtest=org.keycloak.testsuite.adapter.undertow.**.*Test -->
<exclude.undertow.adapter>**/adapter/undertow/**/*Test.java</exclude.undertow.adapter> <exclude.undertow.adapter>**/adapter/undertow/**/*Test.java</exclude.undertow.adapter>
</properties> </properties>
@@ -124,6 +125,7 @@
<exclude>${exclude.client}</exclude> <exclude>${exclude.client}</exclude>
<exclude>${exclude.cluster}</exclude> <exclude>${exclude.cluster}</exclude>
<exclude>${exclude.undertow.adapter}</exclude> <exclude>${exclude.undertow.adapter}</exclude>
<exclude>${exclude.x509}</exclude>
</excludes> </excludes>
</configuration> </configuration>
</plugin> </plugin>
@@ -26,6 +26,9 @@ import org.keycloak.testsuite.client.resources.TestingResource;
import org.keycloak.testsuite.runonserver.*; import org.keycloak.testsuite.runonserver.*;
import org.keycloak.util.JsonSerialization; 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> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/ */
@@ -33,12 +36,22 @@ public class KeycloakTestingClient {
private final ResteasyWebTarget target; private final ResteasyWebTarget target;
private final ResteasyClient client; private final ResteasyClient client;
private static final boolean authServerSslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
KeycloakTestingClient(String serverUrl, ResteasyClient resteasyClient) { 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); 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) { public static KeycloakTestingClient getInstance(String serverUrl) {
return new KeycloakTestingClient(serverUrl, null); return new KeycloakTestingClient(serverUrl, null);
} }
@@ -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();
}
}
@@ -32,8 +32,10 @@ import org.apache.http.message.BasicNameValuePair;
import org.junit.Assert; import org.junit.Assert;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.RSATokenVerifier; import org.keycloak.RSATokenVerifier;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.Keycloak;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.PemUtils; import org.keycloak.common.util.PemUtils;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jwk.JSONWebKeySet;
@@ -61,12 +63,9 @@ import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.Collections; import java.util.*;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @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 SERVER_ROOT = AuthServerTestEnricher.getAuthServerContextRoot();
public static final String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth"; public static final String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
public static final String APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app"; 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; private Keycloak adminClient;
@@ -192,8 +192,38 @@ public class OAuthClient {
fillLoginForm(username, password); 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) { public AccessTokenResponse doAccessTokenRequest(String code, String password) {
CloseableHttpClient client = new DefaultHttpClient(); CloseableHttpClient client = newCloseableHttpClient();
try { try {
HttpPost post = new HttpPost(getAccessTokenUrl()); HttpPost post = new HttpPost(getAccessTokenUrl());
@@ -296,7 +326,7 @@ public class OAuthClient {
public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp, public AccessTokenResponse doGrantAccessTokenRequest(String realm, String username, String password, String totp,
String clientId, String clientSecret) throws Exception { String clientId, String clientSecret) throws Exception {
CloseableHttpClient client = new DefaultHttpClient(); CloseableHttpClient client = newCloseableHttpClient();
try { try {
HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm)); HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
@@ -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-spnego", "Kerberos", "Initiates the SPNEGO protocol. Most often used with Kerberos.");
addProviderInfo(result, "auth-username-password-form", "Username Password Form", addProviderInfo(result, "auth-username-password-form", "Username Password Form",
"Validates a username and password from login 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-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request");
addProviderInfo(result, "direct-grant-validate-password", "Password", addProviderInfo(result, "direct-grant-validate-password", "Password",
"Validates the password supplied as a 'password' form parameter in direct grant request"); "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"); "Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru", addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru",
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user"); "Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
return result; return result;
} }
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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", "");
}
}
@@ -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();
}
}
@@ -26,7 +26,7 @@
<property name="browser">${browser}</property> <property name="browser">${browser}</property>
<property name="htmlUnit.version">${htmlUnitBrowserVersion}</property> <property name="htmlUnit.version">${htmlUnitBrowserVersion}</property>
<property name="firefox_binary">${firefox_binary}</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>
<extension qualifier="graphene"> <extension qualifier="graphene">
@@ -173,4 +173,4 @@
</configuration> </configuration>
</container> </container>
</arquillian> </arquillian>
@@ -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-----
@@ -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-----
@@ -83,6 +83,16 @@
<testsuite.constants>${project.build.directory}/dependency/test-constants.properties</testsuite.constants> <testsuite.constants>${project.build.directory}/dependency/test-constants.properties</testsuite.constants>
<skip.add.user.json>false</skip.add.user.json> <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> </properties>
<build> <build>
@@ -212,6 +222,17 @@
<project.version>${project.version}</project.version> <project.version>${project.version}</project.version>
<migration.project.version>${migration.project.version}</migration.project.version> <migration.project.version>${migration.project.version}</migration.project.version>
<migration.product.version>${migration.product.version}</migration.product.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> </systemPropertyVariables>
<properties> <properties>
<property> <property>
@@ -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>