/*
* 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) {
}
}