KEYCLOAK-10158 Use PEM cert as X.509 user identity
Allows to use the full PEM encoded X.509 certificate from client cert authentication as a user identity. Also allows to validate that user's identity against LDAP in PEM (String and binary format). In addition, a new custom attribute mapper allows to validate against LDAP when certificate is stored in DER format (binay, Octet-String). KEYCLOAK-10158 Allow lookup of certs in binary adn DER format from LDAP
This commit is contained in:
parent
ca4e14fbfa
commit
c883c11e7e
22 changed files with 320 additions and 23 deletions
|
@ -142,9 +142,13 @@ public final class PemUtils {
|
|||
}
|
||||
}
|
||||
|
||||
private static byte[] pemToDer(String pem) throws IOException {
|
||||
public static byte[] pemToDer(String pem) {
|
||||
try {
|
||||
pem = removeBeginEnd(pem);
|
||||
return Base64.decode(pem);
|
||||
} catch (IOException ioe) {
|
||||
throw new PemException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
public static String removeBeginEnd(String pem) {
|
||||
|
@ -155,11 +159,11 @@ public final class PemUtils {
|
|||
return pem.trim();
|
||||
}
|
||||
|
||||
public static String generateThumbprint(String[] certChain, String encoding) throws NoSuchAlgorithmException, IOException {
|
||||
public static String generateThumbprint(String[] certChain, String encoding) throws NoSuchAlgorithmException {
|
||||
return Base64Url.encode(generateThumbprintBytes(certChain, encoding));
|
||||
}
|
||||
|
||||
static byte[] generateThumbprintBytes(String[] certChain, String encoding) throws NoSuchAlgorithmException, IOException {
|
||||
static byte[] generateThumbprintBytes(String[] certChain, String encoding) throws NoSuchAlgorithmException {
|
||||
return MessageDigest.getInstance(encoding).digest(pemToDer(certChain[0]));
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ public class RSAPublicJWK extends JWK {
|
|||
try {
|
||||
sha1x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-1");
|
||||
sha256x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-256");
|
||||
} catch (NoSuchAlgorithmException | IOException e) {
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,4 +39,8 @@ public interface Condition {
|
|||
|
||||
void applyCondition(StringBuilder filter);
|
||||
|
||||
void setBinary(boolean binary);
|
||||
|
||||
boolean isBinary();
|
||||
|
||||
}
|
|
@ -84,12 +84,33 @@ public enum EscapeStrategy {
|
|||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
// Escaping value as Octet-String
|
||||
OCTET_STRING {
|
||||
@Override
|
||||
public String escape(String input) {
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = input.getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return escapeHex(bytes);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public static String escapeHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("\\%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public abstract String escape(String input);
|
||||
|
||||
|
||||
protected void appendByte(byte b, StringBuilder output) {
|
||||
if (b >= 0) {
|
||||
output.append((char) b);
|
||||
|
|
|
@ -48,4 +48,13 @@ class CustomLDAPFilter implements Condition {
|
|||
public void applyCondition(StringBuilder filter) {
|
||||
filter.append(customFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBinary(boolean binary) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBinary() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ import java.util.Date;
|
|||
*/
|
||||
public class EqualCondition extends NamedParameterCondition {
|
||||
|
||||
private final Object value;
|
||||
private final EscapeStrategy escapeStrategy;
|
||||
private Object value;
|
||||
|
||||
public EqualCondition(String name, Object value, EscapeStrategy escapeStrategy) {
|
||||
super(name);
|
||||
|
@ -41,6 +41,10 @@ public class EqualCondition extends NamedParameterCondition {
|
|||
return this.value;
|
||||
}
|
||||
|
||||
public void setValue(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public EscapeStrategy getEscapeStrategy() {
|
||||
return escapeStrategy;
|
||||
}
|
||||
|
@ -52,7 +56,7 @@ public class EqualCondition extends NamedParameterCondition {
|
|||
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
|
||||
}
|
||||
|
||||
String escaped = escapeStrategy.escape(parameterValue.toString());
|
||||
String escaped = new OctetStringEncoder(escapeStrategy).encode(parameterValue, isBinary());
|
||||
|
||||
filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(escaped).append(")");
|
||||
}
|
||||
|
|
|
@ -50,4 +50,13 @@ class GreaterThanCondition extends NamedParameterCondition {
|
|||
filter.append("(").append(getParameterName()).append(">").append(parameterValue).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBinary(boolean binary) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBinary() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ class InCondition extends NamedParameterCondition {
|
|||
filter.append("(&(");
|
||||
|
||||
for (int i = 0; i< valuesToCompare.length; i++) {
|
||||
Object value = valuesToCompare[i];
|
||||
Object value = new OctetStringEncoder().encode(valuesToCompare[i], isBinary());
|
||||
|
||||
filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(value).append(")");
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.storage.ldap.idm.query.Condition;
|
|||
public abstract class NamedParameterCondition implements Condition {
|
||||
|
||||
private String parameterName;
|
||||
private boolean binary;
|
||||
|
||||
public NamedParameterCondition(String parameterName) {
|
||||
this.parameterName = parameterName;
|
||||
|
@ -47,4 +48,14 @@ public abstract class NamedParameterCondition implements Condition {
|
|||
this.parameterName = ldapParamName;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBinary(boolean binary) {
|
||||
this.binary = binary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBinary() {
|
||||
return binary;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package org.keycloak.storage.ldap.idm.query.internal;
|
||||
|
||||
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
|
||||
|
||||
class OctetStringEncoder {
|
||||
|
||||
private final EscapeStrategy fallback;
|
||||
|
||||
OctetStringEncoder() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
OctetStringEncoder(EscapeStrategy fallback) {
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
|
||||
public String encode(Object parameterValue, boolean isBinary) {
|
||||
String escaped;
|
||||
if (parameterValue instanceof byte[]) {
|
||||
escaped = EscapeStrategy.escapeHex((byte[]) parameterValue);
|
||||
} else {
|
||||
escaped = escapeAsString(parameterValue, isBinary);
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
private String escapeAsString(Object parameterValue, boolean isBinary) {
|
||||
String escaped;
|
||||
String stringValue = parameterValue.toString();
|
||||
if (isBinary) {
|
||||
escaped = EscapeStrategy.OCTET_STRING.escape(stringValue);
|
||||
} else if (fallback == null){
|
||||
escaped = stringValue;
|
||||
} else {
|
||||
escaped = fallback.escape(stringValue);
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
}
|
|
@ -56,4 +56,13 @@ class OrCondition implements Condition {
|
|||
|
||||
filter.append(")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBinary(boolean binary) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBinary() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package org.keycloak.storage.ldap.mappers;
|
||||
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
import org.keycloak.storage.ldap.idm.query.Condition;
|
||||
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
|
||||
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
||||
|
||||
public class CertificateLDAPStorageMapper extends UserAttributeLDAPStorageMapper {
|
||||
|
||||
public static final String IS_DER_FORMATTED = "is.der.formatted";
|
||||
|
||||
public CertificateLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
|
||||
super(mapperModel, ldapProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
super.beforeLDAPQuery(query);
|
||||
|
||||
String ldapAttrName = getLdapAttributeName();
|
||||
|
||||
if (isDerFormatted()) {
|
||||
for (Condition condition : query.getConditions()) {
|
||||
if (condition instanceof EqualCondition &&
|
||||
condition.getParameterName().equalsIgnoreCase(ldapAttrName)) {
|
||||
EqualCondition equalCondition = ((EqualCondition) condition);
|
||||
equalCondition.setValue(PemUtils.pemToDer(equalCondition.getValue().toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDerFormatted() {
|
||||
return mapperModel.get(IS_DER_FORMATTED, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package org.keycloak.storage.ldap.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
|
||||
public class CertificateLDAPStorageMapperFactory extends UserAttributeLDAPStorageMapperFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "certificate-ldap-mapper";
|
||||
|
||||
private static final List<ProviderConfigProperty> certificateConfigProperties;
|
||||
|
||||
static {
|
||||
certificateConfigProperties = getCertificateConfigProperties(null);
|
||||
}
|
||||
|
||||
private static List<ProviderConfigProperty> getCertificateConfigProperties(ComponentModel p) {
|
||||
List<ProviderConfigProperty> configProps = new ArrayList<>(getConfigProps(null));
|
||||
|
||||
ProviderConfigurationBuilder config = ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name(CertificateLDAPStorageMapper.IS_DER_FORMATTED)
|
||||
.label("DER Formatted")
|
||||
.helpText("Activate this if the certificate is DER formatted in LDAP and not PEM formatted.")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.add();
|
||||
configProps.addAll(config.build());
|
||||
return configProps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Used to map single attribute which contains a certificate from LDAP user to attribute of UserModel in Keycloak DB";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return certificateConfigProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
|
||||
super.validateConfiguration(session, realm, config);
|
||||
|
||||
boolean isBinaryAttribute = config.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
|
||||
boolean isDerFormatted = config.get(CertificateLDAPStorageMapper.IS_DER_FORMATTED, false);
|
||||
if (isDerFormatted && !isBinaryAttribute) {
|
||||
throw new ComponentValidationException("With DER formatted certificate enabled, the ''Is Binary Attribute'' option must be enabled too");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider) {
|
||||
return new CertificateLDAPStorageMapper(mapperModel, federationProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
|
||||
return getCertificateConfigProperties(parent);
|
||||
}
|
||||
|
||||
}
|
|
@ -89,12 +89,11 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
|||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
||||
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
||||
String userModelAttrName = getUserModelAttribute();
|
||||
String ldapAttrName = getLdapAttributeName();
|
||||
|
||||
// We won't update binary attributes to Keycloak DB. They might be too big
|
||||
boolean isBinaryAttribute = mapperModel.get(IS_BINARY_ATTRIBUTE, false);
|
||||
if (isBinaryAttribute) {
|
||||
if (isBinaryAttribute()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -122,8 +121,8 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
|||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
||||
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
||||
String userModelAttrName = getUserModelAttribute();
|
||||
String ldapAttrName = getLdapAttributeName();
|
||||
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||
|
||||
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
|
||||
|
@ -201,8 +200,8 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
|||
|
||||
@Override
|
||||
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
||||
final String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
||||
final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
||||
final String userModelAttrName = getUserModelAttribute();
|
||||
final String ldapAttrName = getLdapAttributeName();
|
||||
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
|
||||
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||
final boolean isBinaryAttribute = parseBooleanParameter(mapperModel, IS_BINARY_ATTRIBUTE);
|
||||
|
@ -416,8 +415,8 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
|||
|
||||
@Override
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
||||
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
||||
String userModelAttrName = getUserModelAttribute();
|
||||
String ldapAttrName = getLdapAttributeName();
|
||||
|
||||
// Add mapped attribute to returning ldap attributes
|
||||
query.addReturningLdapAttribute(ldapAttrName);
|
||||
|
@ -428,8 +427,24 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
|||
// Change conditions and use ldapAttribute instead of userModel
|
||||
for (Condition condition : query.getConditions()) {
|
||||
condition.updateParameterName(userModelAttrName, ldapAttrName);
|
||||
String parameterName = condition.getParameterName();
|
||||
if (parameterName != null && (parameterName.equalsIgnoreCase(userModelAttrName) || parameterName.equalsIgnoreCase(ldapAttrName))) {
|
||||
condition.setBinary(isBinaryAttribute());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getUserModelAttribute() {
|
||||
return mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
||||
}
|
||||
|
||||
String getLdapAttributeName() {
|
||||
return mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
||||
}
|
||||
|
||||
private boolean isBinaryAttribute() {
|
||||
return mapperModel.get(IS_BINARY_ATTRIBUTE, false);
|
||||
}
|
||||
|
||||
private boolean isReadOnly() {
|
||||
return parseBooleanParameter(mapperModel, READ_ONLY);
|
||||
|
|
|
@ -43,7 +43,7 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
|
|||
configProperties = props;
|
||||
}
|
||||
|
||||
private static List<ProviderConfigProperty> getConfigProps(ComponentModel p) {
|
||||
static List<ProviderConfigProperty> getConfigProps(ComponentModel p) {
|
||||
String readOnly = "false";
|
||||
UserStorageProviderModel parent = new UserStorageProviderModel();
|
||||
if (p != null) {
|
||||
|
|
|
@ -24,3 +24,4 @@ org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory
|
|||
org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory
|
||||
org.keycloak.storage.ldap.mappers.msadlds.MSADLDSUserAccountControlStorageMapperFactory
|
||||
org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory
|
||||
org.keycloak.storage.ldap.mappers.CertificateLDAPStorageMapperFactory
|
||||
|
|
|
@ -60,6 +60,10 @@
|
|||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>javax.json</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi</artifactId>
|
||||
|
|
|
@ -68,6 +68,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
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_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";
|
||||
public static final String USERNAME_EMAIL_MAPPER = "Username or Email";
|
||||
|
@ -174,6 +175,9 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
.either(UserIdentityExtractor.getX500NameExtractor(BCStyle.EmailAddress, issuer))
|
||||
.or(UserIdentityExtractor.getX500NameExtractor(BCStyle.E, issuer));
|
||||
break;
|
||||
case CERTIFICATE_PEM:
|
||||
extractor = UserIdentityExtractor.getCertificatePemIdentityExtractor(config);
|
||||
break;
|
||||
default:
|
||||
logger.warnf("[UserIdentityExtractorBuilder:fromConfig] Unknown or unsupported user identity source: \"%s\"", userIdentitySource.getName());
|
||||
break;
|
||||
|
|
|
@ -79,7 +79,8 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
|
|||
MAPPING_SOURCE_CERT_ISSUERDN,
|
||||
MAPPING_SOURCE_CERT_ISSUERDN_EMAIL,
|
||||
MAPPING_SOURCE_CERT_ISSUERDN_CN,
|
||||
MAPPING_SOURCE_CERT_SERIALNUMBER
|
||||
MAPPING_SOURCE_CERT_SERIALNUMBER,
|
||||
MAPPING_SOURCE_CERT_CERTIFICATE_PEM
|
||||
};
|
||||
|
||||
private static final String[] userModelMappers = {
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.bouncycastle.asn1.DERUTF8String;
|
|||
import org.bouncycastle.asn1.x500.RDN;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
@ -275,4 +276,19 @@ public abstract class UserIdentityExtractor {
|
|||
public static OrBuilder either(UserIdentityExtractor extractor) {
|
||||
return new OrBuilder(extractor);
|
||||
}
|
||||
|
||||
public static UserIdentityExtractor getCertificatePemIdentityExtractor(X509AuthenticatorConfigModel config) {
|
||||
return new UserIdentityExtractor() {
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
if (certs == null || certs.length == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
String pem = PemUtils.encodeCertificate(certs[0]);
|
||||
logger.debugf("Using PEM certificate \"%s\" as user identity.", pem);
|
||||
return pem;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,8 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
|
|||
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);
|
||||
SUBJECTDN(MAPPING_SOURCE_CERT_SUBJECTDN),
|
||||
CERTIFICATE_PEM(MAPPING_SOURCE_CERT_CERTIFICATE_PEM);
|
||||
|
||||
private String name;
|
||||
MappingSourceType(String name) {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
|
||||
public class CertificatePemIdentityExtractorTest {
|
||||
|
||||
@Test
|
||||
public void testExtractsCertInPemFormat() throws Exception {
|
||||
InputStream is = getClass().getResourceAsStream("/certs/UPN-cert.pem");
|
||||
X509Certificate x509Certificate = PemUtils.decodeCertificate(StreamUtil.readString(is, Charset.defaultCharset()));
|
||||
String certificatePem = PemUtils.encodeCertificate(x509Certificate);
|
||||
|
||||
X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel();
|
||||
UserIdentityExtractor extractor = UserIdentityExtractor.getCertificatePemIdentityExtractor(config);
|
||||
|
||||
String userIdentity = (String) extractor.extractUserIdentity(new X509Certificate[]{x509Certificate});
|
||||
|
||||
assertEquals(certificatePem, userIdentity);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue