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) {
|
||||||
pem = removeBeginEnd(pem);
|
try {
|
||||||
return Base64.decode(pem);
|
pem = removeBeginEnd(pem);
|
||||||
|
return Base64.decode(pem);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new PemException(ioe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String removeBeginEnd(String pem) {
|
public static String removeBeginEnd(String pem) {
|
||||||
|
@ -155,11 +159,11 @@ public final class PemUtils {
|
||||||
return pem.trim();
|
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));
|
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]));
|
return MessageDigest.getInstance(encoding).digest(pemToDer(certChain[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ public class RSAPublicJWK extends JWK {
|
||||||
try {
|
try {
|
||||||
sha1x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-1");
|
sha1x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-1");
|
||||||
sha256x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-256");
|
sha256x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-256");
|
||||||
} catch (NoSuchAlgorithmException | IOException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,4 +39,8 @@ public interface Condition {
|
||||||
|
|
||||||
void applyCondition(StringBuilder filter);
|
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);
|
public abstract String escape(String input);
|
||||||
|
|
||||||
|
|
||||||
protected void appendByte(byte b, StringBuilder output) {
|
protected void appendByte(byte b, StringBuilder output) {
|
||||||
if (b >= 0) {
|
if (b >= 0) {
|
||||||
output.append((char) b);
|
output.append((char) b);
|
||||||
|
|
|
@ -48,4 +48,13 @@ class CustomLDAPFilter implements Condition {
|
||||||
public void applyCondition(StringBuilder filter) {
|
public void applyCondition(StringBuilder filter) {
|
||||||
filter.append(customFilter);
|
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 {
|
public class EqualCondition extends NamedParameterCondition {
|
||||||
|
|
||||||
private final Object value;
|
|
||||||
private final EscapeStrategy escapeStrategy;
|
private final EscapeStrategy escapeStrategy;
|
||||||
|
private Object value;
|
||||||
|
|
||||||
public EqualCondition(String name, Object value, EscapeStrategy escapeStrategy) {
|
public EqualCondition(String name, Object value, EscapeStrategy escapeStrategy) {
|
||||||
super(name);
|
super(name);
|
||||||
|
@ -41,6 +41,10 @@ public class EqualCondition extends NamedParameterCondition {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setValue(Object value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
public EscapeStrategy getEscapeStrategy() {
|
public EscapeStrategy getEscapeStrategy() {
|
||||||
return escapeStrategy;
|
return escapeStrategy;
|
||||||
}
|
}
|
||||||
|
@ -52,7 +56,7 @@ public class EqualCondition extends NamedParameterCondition {
|
||||||
parameterValue = LDAPUtil.formatDate((Date) parameterValue);
|
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(")");
|
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(")");
|
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("(&(");
|
filter.append("(&(");
|
||||||
|
|
||||||
for (int i = 0; i< valuesToCompare.length; i++) {
|
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(")");
|
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 {
|
public abstract class NamedParameterCondition implements Condition {
|
||||||
|
|
||||||
private String parameterName;
|
private String parameterName;
|
||||||
|
private boolean binary;
|
||||||
|
|
||||||
public NamedParameterCondition(String parameterName) {
|
public NamedParameterCondition(String parameterName) {
|
||||||
this.parameterName = parameterName;
|
this.parameterName = parameterName;
|
||||||
|
@ -47,4 +48,14 @@ public abstract class NamedParameterCondition implements Condition {
|
||||||
this.parameterName = ldapParamName;
|
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(")");
|
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
|
@Override
|
||||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||||
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
String userModelAttrName = getUserModelAttribute();
|
||||||
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
String ldapAttrName = getLdapAttributeName();
|
||||||
|
|
||||||
// We won't update binary attributes to Keycloak DB. They might be too big
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,8 +121,8 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||||
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
String userModelAttrName = getUserModelAttribute();
|
||||||
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
String ldapAttrName = getLdapAttributeName();
|
||||||
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||||
|
|
||||||
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
|
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
|
||||||
|
@ -201,8 +200,8 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
||||||
final String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
final String userModelAttrName = getUserModelAttribute();
|
||||||
final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
final String ldapAttrName = getLdapAttributeName();
|
||||||
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
|
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
|
||||||
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||||
final boolean isBinaryAttribute = parseBooleanParameter(mapperModel, IS_BINARY_ATTRIBUTE);
|
final boolean isBinaryAttribute = parseBooleanParameter(mapperModel, IS_BINARY_ATTRIBUTE);
|
||||||
|
@ -416,8 +415,8 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeLDAPQuery(LDAPQuery query) {
|
public void beforeLDAPQuery(LDAPQuery query) {
|
||||||
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
String userModelAttrName = getUserModelAttribute();
|
||||||
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
String ldapAttrName = getLdapAttributeName();
|
||||||
|
|
||||||
// Add mapped attribute to returning ldap attributes
|
// Add mapped attribute to returning ldap attributes
|
||||||
query.addReturningLdapAttribute(ldapAttrName);
|
query.addReturningLdapAttribute(ldapAttrName);
|
||||||
|
@ -428,9 +427,25 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||||
// Change conditions and use ldapAttribute instead of userModel
|
// Change conditions and use ldapAttribute instead of userModel
|
||||||
for (Condition condition : query.getConditions()) {
|
for (Condition condition : query.getConditions()) {
|
||||||
condition.updateParameterName(userModelAttrName, ldapAttrName);
|
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() {
|
private boolean isReadOnly() {
|
||||||
return parseBooleanParameter(mapperModel, READ_ONLY);
|
return parseBooleanParameter(mapperModel, READ_ONLY);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
|
||||||
configProperties = props;
|
configProperties = props;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ProviderConfigProperty> getConfigProps(ComponentModel p) {
|
static List<ProviderConfigProperty> getConfigProps(ComponentModel p) {
|
||||||
String readOnly = "false";
|
String readOnly = "false";
|
||||||
UserStorageProviderModel parent = new UserStorageProviderModel();
|
UserStorageProviderModel parent = new UserStorageProviderModel();
|
||||||
if (p != null) {
|
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.msad.MSADUserAccountControlStorageMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.msadlds.MSADLDSUserAccountControlStorageMapperFactory
|
org.keycloak.storage.ldap.mappers.msadlds.MSADLDSUserAccountControlStorageMapperFactory
|
||||||
org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory
|
org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory
|
||||||
|
org.keycloak.storage.ldap.mappers.CertificateLDAPStorageMapperFactory
|
||||||
|
|
|
@ -60,6 +60,10 @@
|
||||||
<groupId>org.glassfish</groupId>
|
<groupId>org.glassfish</groupId>
|
||||||
<artifactId>javax.json</artifactId>
|
<artifactId>javax.json</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-common</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-server-spi</artifactId>
|
<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_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_ISSUERDN_CN = "Issuer's Common Name";
|
||||||
public static final String MAPPING_SOURCE_CERT_SERIALNUMBER = "Certificate Serial Number";
|
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_MAPPER_SELECTION = "x509-cert-auth.mapper-selection";
|
||||||
public static final String USER_ATTRIBUTE_MAPPER = "Custom Attribute Mapper";
|
public static final String USER_ATTRIBUTE_MAPPER = "Custom Attribute Mapper";
|
||||||
public static final String USERNAME_EMAIL_MAPPER = "Username or Email";
|
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))
|
.either(UserIdentityExtractor.getX500NameExtractor(BCStyle.EmailAddress, issuer))
|
||||||
.or(UserIdentityExtractor.getX500NameExtractor(BCStyle.E, issuer));
|
.or(UserIdentityExtractor.getX500NameExtractor(BCStyle.E, issuer));
|
||||||
break;
|
break;
|
||||||
|
case CERTIFICATE_PEM:
|
||||||
|
extractor = UserIdentityExtractor.getCertificatePemIdentityExtractor(config);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logger.warnf("[UserIdentityExtractorBuilder:fromConfig] Unknown or unsupported user identity source: \"%s\"", userIdentitySource.getName());
|
logger.warnf("[UserIdentityExtractorBuilder:fromConfig] Unknown or unsupported user identity source: \"%s\"", userIdentitySource.getName());
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -79,7 +79,8 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
|
||||||
MAPPING_SOURCE_CERT_ISSUERDN,
|
MAPPING_SOURCE_CERT_ISSUERDN,
|
||||||
MAPPING_SOURCE_CERT_ISSUERDN_EMAIL,
|
MAPPING_SOURCE_CERT_ISSUERDN_EMAIL,
|
||||||
MAPPING_SOURCE_CERT_ISSUERDN_CN,
|
MAPPING_SOURCE_CERT_ISSUERDN_CN,
|
||||||
MAPPING_SOURCE_CERT_SERIALNUMBER
|
MAPPING_SOURCE_CERT_SERIALNUMBER,
|
||||||
|
MAPPING_SOURCE_CERT_CERTIFICATE_PEM
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String[] userModelMappers = {
|
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.RDN;
|
||||||
import org.bouncycastle.asn1.x500.X500Name;
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||||
|
import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
@ -275,4 +276,19 @@ public abstract class UserIdentityExtractor {
|
||||||
public static OrBuilder either(UserIdentityExtractor extractor) {
|
public static OrBuilder either(UserIdentityExtractor extractor) {
|
||||||
return new OrBuilder(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),
|
SUBJECTDN_EMAIL(MAPPING_SOURCE_CERT_SUBJECTDN_EMAIL),
|
||||||
SUBJECTALTNAME_EMAIL(MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL),
|
SUBJECTALTNAME_EMAIL(MAPPING_SOURCE_CERT_SUBJECTALTNAME_EMAIL),
|
||||||
SUBJECTALTNAME_OTHERNAME(MAPPING_SOURCE_CERT_SUBJECTALTNAME_OTHERNAME),
|
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;
|
private String name;
|
||||||
MappingSourceType(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