KEYCLOAK-10785 X.509 Authenticator - Update user identity source mappers

Update user identity sources and the way how X.509 certificates are mapped to the user to:
1. Include "Serial number + Issuer DN" as described in RFC 5280
2. Include "Certificate's SHA256-Thumbprint"
3. Exclude "Issuer DN"
4. Exclude "Issuer Email"

Add an option to represent serial number in hexadecimal format.

Documentation PR created: https://github.com/keycloak/keycloak-documentation/pull/714
KEYCLOAK-10785 - Documentation for new user identity source mappers
This commit is contained in:
Nemanja Hiršl 2019-07-08 13:50:50 +02:00 committed by Bruno Oliveira da Silva
parent 75d2ec8ff6
commit 411ea331f6
7 changed files with 134 additions and 40 deletions

View file

@ -29,16 +29,22 @@ 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.bouncycastle.util.encoders.Hex;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.events.Details;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.jose.jws.crypto.HashUtils;
import org.keycloak.crypto.HashException;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.x509.X509ClientCertificateLookup;
/**
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
* @version $Revision: 1 $
@ -55,6 +61,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
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 CANONICAL_DN = "x509-cert-auth.canonical-dn-enabled";
public static final String SERIALNUMBER_HEX = "x509-cert-auth.serialnumber-hex-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 OCSPRESPONDER_CERTIFICATE = "x509-cert-auth.ocsp-responder-certificate";
@ -65,9 +72,9 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
public static final String MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME = "Subject's Alternative Name otherName (UPN)";
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 MAPPING_SOURCE_CERT_SHA256_THUMBPRINT = "SHA-256 Thumbprint";
public static final String MAPPING_SOURCE_CERT_SERIALNUMBER_ISSUERDN = "Certificate Serial Number and IssuerDN";
public static final String MAPPING_SOURCE_CERT_CERTIFICATE_PEM = "Full Certificate in PEM format";
public static final String USER_MAPPER_SELECTION = "x509-cert-auth.mapper-selection";
public static final String USER_ATTRIBUTE_MAPPER = "Custom Attribute Mapper";
@ -130,6 +137,18 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
return null;
};
private static final Function<X509Certificate[], String> getSerialnumberFunc(X509AuthenticatorConfigModel config) {
return config.isSerialnumberHex() ?
certs -> Hex.toHexString(certs[0].getSerialNumber().toByteArray()) :
certs -> certs[0].getSerialNumber().toString();
}
private static final Function<X509Certificate[], String> getIssuerDNFunc(X509AuthenticatorConfigModel config) {
return config.isCanonicalDnEnabled() ?
certs -> certs[0].getIssuerX500Principal().getName(X500Principal.CANONICAL) :
certs -> certs[0].getIssuerDN().getName();
}
static UserIdentityExtractor fromConfig(X509AuthenticatorConfigModel config) {
X509AuthenticatorConfigModel.MappingSourceType userIdentitySource = config.getMappingSourceType();
@ -146,13 +165,24 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, func);
break;
case ISSUERDN:
func = config.isCanonicalDnEnabled() ?
certs -> certs[0].getIssuerX500Principal().getName(X500Principal.CANONICAL) :
certs -> certs[0].getIssuerDN().getName();
extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, func);
extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, getIssuerDNFunc(config));
break;
case SERIALNUMBER:
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> certs[0].getSerialNumber().toString());
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, getSerialnumberFunc(config));
break;
case SHA256_THUMBPRINT:
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> {
try {
return Hex.toHexString(HashUtils.hash(JavaAlgorithm.SHA256, certs[0].getEncoded()));
} catch (CertificateEncodingException | HashException e) {
logger.warn("Unable to get certificate's thumbprint", e);
}
return null;
});
break;
case SERIALNUMBER_ISSUERDN:
func = certs -> getSerialnumberFunc(config).apply(certs) + Constants.CFG_DELIMITER + getIssuerDNFunc(config).apply(certs);
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, func);
break;
case SUBJECTDN_CN:
extractor = UserIdentityExtractor.getX500NameExtractor(BCStyle.CN, subject);
@ -168,14 +198,6 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
case SUBJECTALTNAME_OTHERNAME:
extractor = UserIdentityExtractor.getSubjectAltNameExtractor(0);
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;
case CERTIFICATE_PEM:
extractor = UserIdentityExtractor.getCertificatePemIdentityExtractor(config);
break;

View file

@ -40,8 +40,6 @@ import static org.keycloak.authentication.authenticators.x509.AbstractX509Client
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_SUBJECTALTNAME_EMAIL;
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME;
@ -77,9 +75,9 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME,
MAPPING_SOURCE_CERT_SUBJECTDN_CN,
MAPPING_SOURCE_CERT_ISSUERDN,
MAPPING_SOURCE_CERT_ISSUERDN_EMAIL,
MAPPING_SOURCE_CERT_ISSUERDN_CN,
MAPPING_SOURCE_CERT_SERIALNUMBER,
MAPPING_SOURCE_CERT_SERIALNUMBER_ISSUERDN,
MAPPING_SOURCE_CERT_SHA256_THUMBPRINT,
MAPPING_SOURCE_CERT_CERTIFICATE_PEM
};
@ -109,6 +107,14 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
canonicalDn.setDefaultValue(false);
canonicalDn.setHelpText("Use the canonical format to determine the distinguished name. This option is relevant for authenticators using a distinguished name.");
ProviderConfigProperty serialnumberHex = new ProviderConfigProperty();
serialnumberHex.setType(BOOLEAN_TYPE);
serialnumberHex.setName(SERIALNUMBER_HEX);
serialnumberHex.setLabel("Enable Serial Number hexadecimal representation");
serialnumberHex.setDefaultValue(false);
serialnumberHex.setHelpText("Use the hex representation of the serial number. This option is relevant for authenticators using serial number.");
ProviderConfigProperty regExp = new ProviderConfigProperty();
regExp.setType(STRING_TYPE);
regExp.setName(REGULAR_EXPRESSION);
@ -130,11 +136,12 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
userMapperList.setOptions(mapperTypes);
ProviderConfigProperty attributeOrPropertyValue = new ProviderConfigProperty();
attributeOrPropertyValue.setType(STRING_TYPE);
attributeOrPropertyValue.setType(MULTIVALUED_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.");
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. " +
"Multiple values are relevant when attribute mapping is related to multiple values, e.g. 'Certificate Serial Number and IssuerDN'");
ProviderConfigProperty crlCheckingEnabled = new ProviderConfigProperty();
crlCheckingEnabled.setType(BOOLEAN_TYPE);
@ -198,6 +205,7 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
configProperties = asList(mappingMethodList,
canonicalDn,
serialnumberHex,
regExp,
userMapperList,
attributeOrPropertyValue,

View file

@ -37,6 +37,7 @@ import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -222,7 +223,7 @@ public abstract class UserIdentityExtractor {
@Override
public Object extractUserIdentity(X509Certificate[] certs) {
String value = _f.apply(certs);
String value = Optional.ofNullable(_f.apply(certs)).orElseThrow(IllegalArgumentException::new);
Pattern r = Pattern.compile(_pattern, Pattern.CASE_INSENSITIVE);

View file

@ -18,9 +18,12 @@
package org.keycloak.authentication.authenticators.x509;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserModel;
@ -45,16 +48,27 @@ public abstract class UserIdentityToModelMapper {
}
static class UserIdentityToCustomAttributeMapper extends UserIdentityToModelMapper {
private String _customAttribute;
UserIdentityToCustomAttributeMapper(String customAttribute) {
_customAttribute = customAttribute;
private List<String> _customAttributes;
UserIdentityToCustomAttributeMapper(String customAttributes) {
_customAttributes = Arrays.asList(Constants.CFG_DELIMITER_PATTERN.split(customAttributes));
}
@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());
List<String> userIdentityValues = Arrays.asList(Constants.CFG_DELIMITER_PATTERN.split(userIdentity.toString()));
if (_customAttributes.isEmpty() || userIdentityValues.isEmpty() || (_customAttributes.size() != userIdentityValues.size())) {
return null;
}
List<UserModel> users = session.users().searchForUserByUserAttribute(_customAttributes.get(0), userIdentityValues.get(0), context.getRealm());
for (int i = 1; i <_customAttributes.size(); ++i) {
String customAttribute = _customAttributes.get(i);
String userIdentityValue = userIdentityValues.get(i);
users = users.stream().filter(user -> user.getFirstAttribute(customAttribute).equals(userIdentityValue)).collect(Collectors.toList());
}
if (users != null && users.size() > 1) {
throw new ModelDuplicateException();
}

View file

@ -55,14 +55,14 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
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),
SUBJECTALTNAME_EMAIL(MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL),
SUBJECTALTNAME_OTHERNAME(MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME),
SUBJECTDN(MAPPING_SOURCE_CERT_SUBJECTDN),
SHA256_THUMBPRINT(MAPPING_SOURCE_CERT_SHA256_THUMBPRINT),
SERIALNUMBER_ISSUERDN(MAPPING_SOURCE_CERT_SERIALNUMBER_ISSUERDN),
CERTIFICATE_PEM(MAPPING_SOURCE_CERT_CERTIFICATE_PEM);
private String name;
@ -253,4 +253,13 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
getConfig().put(CANONICAL_DN, Boolean.toString(value));
return this;
}
public boolean isSerialnumberHex() {
return Boolean.parseBoolean(getConfig().get(SERIALNUMBER_HEX));
}
public X509AuthenticatorConfigModel setSerialnumberHex(boolean value) {
getConfig().put(SERIALNUMBER_HEX, Boolean.toString(value));
return this;
}
}

View file

@ -76,7 +76,6 @@ 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.SUBJECTALTNAME_EMAIL;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTALTNAME_OTHERNAME;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN;

View file

@ -40,8 +40,8 @@ import static org.hamcrest.Matchers.startsWith;
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_CN;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.ISSUERDN_EMAIL;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SERIALNUMBER_ISSUERDN;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SHA256_THUMBPRINT;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SERIALNUMBER;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
@ -146,9 +146,34 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
}
@Test
public void loginAsUserFromCertIssuerCNMappedToUserAttribute() {
x509BrowserLogin(createLoginWithSpecifiedSourceTypeToCustomAttributeConfig(ISSUERDN_CN, "x509_issuer_identity"),
userId2, "keycloak", "Keycloak Intermediate CA");
public void loginAsUserFromCertSerialnumberAndIssuerDNMappedToUserAttribute() {
UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
Assert.assertNotNull(user);
user.singleAttribute("x509_certificate_serialnumber", "4105");
user.singleAttribute("x509_issuer_dn", "EMAILADDRESS=contact@keycloak.org, CN=Keycloak Intermediate CA, OU=Keycloak, O=Red Hat, ST=MA, C=US");
this.updateUser(user);
events.clear();
x509BrowserLogin(createLoginWithSpecifiedSourceTypeToCustomAttributeConfig(SERIALNUMBER_ISSUERDN, "x509_certificate_serialnumber##x509_issuer_dn"),
userId2, "keycloak", "4105##EMAILADDRESS=contact@keycloak.org, CN=Keycloak Intermediate CA, OU=Keycloak, O=Red Hat, ST=MA, C=US");
}
@Test
public void loginAsUserFromHexCertSerialnumberAndIssuerDNMappedToUserAttribute() {
UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
Assert.assertNotNull(user);
user.singleAttribute("x509_certificate_serialnumber", "1009");
user.singleAttribute("x509_issuer_dn", "EMAILADDRESS=contact@keycloak.org, CN=Keycloak Intermediate CA, OU=Keycloak, O=Red Hat, ST=MA, C=US");
this.updateUser(user);
events.clear();
X509AuthenticatorConfigModel config = createLoginWithSpecifiedSourceTypeToCustomAttributeConfig(SERIALNUMBER_ISSUERDN, "x509_certificate_serialnumber##x509_issuer_dn");
config.setSerialnumberHex(true);
x509BrowserLogin(config, userId2, "keycloak", "1009##EMAILADDRESS=contact@keycloak.org, CN=Keycloak Intermediate CA, OU=Keycloak, O=Red Hat, ST=MA, C=US");
}
@Test
@ -167,18 +192,18 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
@Test
public void loginAsUserFromCertIssuerEmailMappedToUserAttribute() {
public void loginAsUserFromCertSHA256MappedToUserAttribute() {
UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
Assert.assertNotNull(user);
user.singleAttribute("x509_issuer_identity", "contact@keycloak.org");
user.singleAttribute("x509_cert_sha256thumbprint", "71237a14c118a90cc8406f14d039ed3431c9065f68e535293ee919d4c33b5e15");
this.updateUser(user);
events.clear();
x509BrowserLogin(createLoginWithSpecifiedSourceTypeToCustomAttributeConfig(ISSUERDN_EMAIL, "x509_issuer_identity"),
userId2, "keycloak", "contact@keycloak.org");
x509BrowserLogin(createLoginWithSpecifiedSourceTypeToCustomAttributeConfig(SHA256_THUMBPRINT, "x509_cert_sha256thumbprint"),
userId2, "keycloak", "71237a14c118a90cc8406f14d039ed3431c9065f68e535293ee919d4c33b5e15");
}
@ -197,6 +222,22 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
userId2, "keycloak", "4105");
}
@Test
public void loginAsUserFromHexCertSerialNumberMappedToUserAttribute() {
UserRepresentation user = testRealm().users().get(userId2).toRepresentation();
Assert.assertNotNull(user);
user.singleAttribute("x509_serial_number", "1009");
this.updateUser(user);
events.clear();
X509AuthenticatorConfigModel config = createLoginWithSpecifiedSourceTypeToCustomAttributeConfig(SERIALNUMBER, "x509_serial_number");
config.setSerialnumberHex(true);
x509BrowserLogin(config, userId2, "keycloak", "1009");
}
@Test
public void loginDuplicateUsersNotAllowed() {