问题
In order to override LDAP connection and redirect password validation to our own cached passwords system, in keycloak, whenever the LDAP connection is lost. The easier way was to create an HAProxy around the LDAP to ensure that it never goes down but we do not have access to this and our client wants to redirect to our cached password system. Nevertheless, the point of this post is to tell how to create a Custom LDAP Storage Provider for keycloak.
(check keycloak documentation).
- Building
Create a Java project (jar) and add the following dependencies
pom.xml (for maven! if using gradle one must add these dependencies as well)
Note: make sure the keycloak version of the dependencies is the same as the running keycloak instance, also note that these dependencies are scope=provided meaning these will already be in the classloader.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>foo.bar</groupId>
<artifactId>custom-ldap-spi</artifactId>
<version>1.0.0</version>
<name>Custom LDAP Provider</name>
<description />
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<keycloak.version>11.0.2</keycloak.version>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-kerberos-federation</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ldap-federation</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>3.9.0.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.4.1.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
<version>1.1.1.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.ejb</groupId>
<artifactId>jboss-ejb-api_3.2_spec</artifactId>
<version>2.0.0.Final</version>
</dependency>
</dependencies>
<build>
<finalName>custom-ldap-provider</finalName>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
Next, we will need to add two classes that extend the existing LDAP provider as we will want all those functionalities as well and just need to tweak a few methods.
The CustomLDAPStorageProvider will extend LDAPStorageProvider and goes as follows:
Note: these overridden methods are for test and debug the is no business logic in here for now as I will be added later as needed. Notice also that on LDAP password validation exceptions we are just saying "true" password is valid. Here is where we will call our own password validator.
package foo.bar.lion.storage.ldap;
import javax.ejb.Remove;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
public class CustomLDAPStorageProvider extends LDAPStorageProvider {
private static final Logger logger = Logger.getLogger(CustomLDAPStorageProvider.class);
public CustomLDAPStorageProvider(CustomLDAPStorageProviderFactory factory, KeycloakSession session,
ComponentModel model, LDAPIdentityStore ldapIdentityStore) {
super(factory, session, model, ldapIdentityStore);
}
@Override
public UserModel validate(RealmModel realm, UserModel local) {
try {
logger.error("####### VALIDATE USER");
return super.validate(realm, local);
} catch (Exception e) {
logger.error("####### ERROR VALIDATE USER ");
logger.error(e);
return null;
}
}
@Override
public boolean validPassword(RealmModel realm, UserModel user, String password) {
try {
logger.error("####### VALIDATE password");
return super.validPassword(realm, user, password);
} catch (Exception e) {
logger.error("####### FOR DEMO PURPOUSE ONLY PASSWORDS WILL ALLWAYS BE CORRECT");
return true;
}
}
@Override
protected LDAPObject loadAndValidateUser(RealmModel realm, UserModel local) {
try {
logger.error("####### LOAD AND VALIDATE USER ");
return super.loadAndValidateUser(realm, local);
} catch (Exception e) {
logger.error("####### Error LOAD AND VALIDATE USER ");
LDAPObject cached = new LDAPObject();
cached.setUuid(local.getId());
return cached;
}
}
@Override
public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) {
logger.error("####### LOAD BY USER MANE " + username);
LDAPObject user = super.loadLDAPUserByUsername(realm, username);
return user;
}
@Remove
@Override
public void close() {
// according to
// https://www.keycloak.org/docs/latest/server_development/#leveraging-java-ee
}
}
The CustomLDAPStorageProviderFactory will extend LDAPStorageProviderFactory and will be loaded only once and will be always the same instance throughout the keycloak uptime.
Note: The getId() will display the name of this provider in the keycloak user federation area. The create() must be overridden in order to instantiate our custom provider. I also had to override the getConfigProperties() because, although all the configs were showing up in the federation manager there were no labels on the HTML boxes and this was a workaround for now but I guess it has something to do with config decorators. need to check that in the future.
package foo.bar.lion.storage.ldap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.keycloak.Config;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPIdentityStoreRegistry;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
public class CustomLDAPStorageProviderFactory extends LDAPStorageProviderFactory
implements ServerInfoAwareProviderFactory {
private LDAPIdentityStoreRegistry ldapStoreRegistry;
@Override
public String getId() {
return "lion-ldap";
}
@Override
public void init(Config.Scope config) {
this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
}
@Override
public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
Map<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(session, model, configDecorators);
return new CustomLDAPStorageProvider(this, session, model, ldapIdentityStore);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
List<ProviderConfigProperty> props = new LinkedList<>();
props.add(new ProviderConfigProperty(LDAPConstants.EDIT_MODE, LDAPConstants.EDIT_MODE, LDAPConstants.EDIT_MODE,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(UserStorageProviderModel.IMPORT_ENABLED,
UserStorageProviderModel.IMPORT_ENABLED, UserStorageProviderModel.IMPORT_ENABLED,
ProviderConfigProperty.BOOLEAN_TYPE, "true"));
props.add(new ProviderConfigProperty(LDAPConstants.SYNC_REGISTRATIONS, LDAPConstants.SYNC_REGISTRATIONS,
LDAPConstants.SYNC_REGISTRATIONS, ProviderConfigProperty.BOOLEAN_TYPE, "false"));
props.add(new ProviderConfigProperty(LDAPConstants.VENDOR, LDAPConstants.VENDOR, LDAPConstants.VENDOR,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP,
LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP, LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP,
ProviderConfigProperty.BOOLEAN_TYPE, "false"));
props.add(
new ProviderConfigProperty(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, LDAPConstants.USERNAME_LDAP_ATTRIBUTE,
LDAPConstants.USERNAME_LDAP_ATTRIBUTE, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.RDN_LDAP_ATTRIBUTE, LDAPConstants.RDN_LDAP_ATTRIBUTE,
LDAPConstants.RDN_LDAP_ATTRIBUTE, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.UUID_LDAP_ATTRIBUTE, LDAPConstants.UUID_LDAP_ATTRIBUTE,
LDAPConstants.UUID_LDAP_ATTRIBUTE, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.USER_OBJECT_CLASSES, LDAPConstants.USER_OBJECT_CLASSES,
LDAPConstants.USER_OBJECT_CLASSES, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_URL, LDAPConstants.CONNECTION_URL,
LDAPConstants.CONNECTION_URL, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.USERS_DN, LDAPConstants.USERS_DN, LDAPConstants.USERS_DN,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE,
ProviderConfigProperty.STRING_TYPE, "simple"));
props.add(new ProviderConfigProperty(LDAPConstants.START_TLS, LDAPConstants.START_TLS, LDAPConstants.START_TLS,
ProviderConfigProperty.BOOLEAN_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.BIND_DN, LDAPConstants.BIND_DN, LDAPConstants.BIND_DN,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.BIND_CREDENTIAL, LDAPConstants.BIND_CREDENTIAL,
LDAPConstants.BIND_CREDENTIAL, ProviderConfigProperty.PASSWORD, "", true));
props.add(new ProviderConfigProperty(LDAPConstants.CUSTOM_USER_SEARCH_FILTER,
LDAPConstants.CUSTOM_USER_SEARCH_FILTER, LDAPConstants.CUSTOM_USER_SEARCH_FILTER,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.SEARCH_SCOPE, LDAPConstants.SEARCH_SCOPE,
LDAPConstants.SEARCH_SCOPE, ProviderConfigProperty.STRING_TYPE, "1"));
props.add(new ProviderConfigProperty(LDAPConstants.VALIDATE_PASSWORD_POLICY,
LDAPConstants.VALIDATE_PASSWORD_POLICY, LDAPConstants.VALIDATE_PASSWORD_POLICY,
ProviderConfigProperty.BOOLEAN_TYPE, "false"));
props.add(new ProviderConfigProperty(LDAPConstants.TRUST_EMAIL, LDAPConstants.TRUST_EMAIL,
LDAPConstants.TRUST_EMAIL, ProviderConfigProperty.BOOLEAN_TYPE, "false"));
props.add(new ProviderConfigProperty(LDAPConstants.USE_TRUSTSTORE_SPI, LDAPConstants.USE_TRUSTSTORE_SPI,
LDAPConstants.USE_TRUSTSTORE_SPI, ProviderConfigProperty.STRING_TYPE, "ldapsOnly"));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING, LDAPConstants.CONNECTION_POOLING,
LDAPConstants.CONNECTION_POOLING, ProviderConfigProperty.BOOLEAN_TYPE, "true"));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_AUTHENTICATION,
LDAPConstants.CONNECTION_POOLING_AUTHENTICATION, LDAPConstants.CONNECTION_POOLING_AUTHENTICATION,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_DEBUG,
LDAPConstants.CONNECTION_POOLING_DEBUG, LDAPConstants.CONNECTION_POOLING_DEBUG,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_INITSIZE,
LDAPConstants.CONNECTION_POOLING_INITSIZE, LDAPConstants.CONNECTION_POOLING_INITSIZE,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_MAXSIZE,
LDAPConstants.CONNECTION_POOLING_MAXSIZE, LDAPConstants.CONNECTION_POOLING_MAXSIZE,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_PREFSIZE,
LDAPConstants.CONNECTION_POOLING_PREFSIZE, LDAPConstants.CONNECTION_POOLING_PREFSIZE,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_PROTOCOL,
LDAPConstants.CONNECTION_POOLING_PROTOCOL, LDAPConstants.CONNECTION_POOLING_PROTOCOL,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_POOLING_TIMEOUT,
LDAPConstants.CONNECTION_POOLING_TIMEOUT, LDAPConstants.CONNECTION_POOLING_TIMEOUT,
ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.CONNECTION_TIMEOUT, LDAPConstants.CONNECTION_TIMEOUT,
LDAPConstants.CONNECTION_TIMEOUT, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.READ_TIMEOUT, LDAPConstants.READ_TIMEOUT,
LDAPConstants.READ_TIMEOUT, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(LDAPConstants.PAGINATION, LDAPConstants.PAGINATION,
LDAPConstants.PAGINATION, ProviderConfigProperty.BOOLEAN_TYPE, "true"));
props.add(new ProviderConfigProperty(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION,
KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION,
ProviderConfigProperty.BOOLEAN_TYPE, "false"));
props.add(new ProviderConfigProperty(KerberosConstants.SERVER_PRINCIPAL, KerberosConstants.SERVER_PRINCIPAL,
KerberosConstants.SERVER_PRINCIPAL, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(KerberosConstants.KEYTAB, KerberosConstants.KEYTAB,
KerberosConstants.KEYTAB, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(KerberosConstants.KERBEROS_REALM, KerberosConstants.KERBEROS_REALM,
KerberosConstants.KERBEROS_REALM, ProviderConfigProperty.STRING_TYPE, ""));
props.add(new ProviderConfigProperty(KerberosConstants.DEBUG, KerberosConstants.DEBUG, KerberosConstants.DEBUG,
ProviderConfigProperty.BOOLEAN_TYPE, "false"));
props.add(new ProviderConfigProperty(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION,
KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION,
KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, ProviderConfigProperty.BOOLEAN_TYPE,
"false"));
props.add(new ProviderConfigProperty(KerberosConstants.SERVER_PRINCIPAL, KerberosConstants.SERVER_PRINCIPAL,
KerberosConstants.SERVER_PRINCIPAL, ProviderConfigProperty.STRING_TYPE, ""));
return props;
}
@Override
public Map<String, String> getOperationalInfo() {
Map<String, String> ret = new LinkedHashMap<>();
ret.put("custom-ldap", "lion-ldap");
return ret;
}
}
Next we have to tell Kecloak that we have a new User Storage Provider and this is done by adding to our project the following file.
in src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory (create this file and add the next line)
foo.bar.lion.storage.ldap.CustomLDAPStorageProviderFactory
We also need to add a Jboss file to tell what we depend on so we create this file:
in src/main/resources/META-INF/jboss-deployment-structure.xml
Note that incorrect dependencies here may lead to (Caused by: "java.util.ServiceConfigurationError": org.keycloak.foo.bar: org.keycloak.foo.Abar not a subtype)
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.keycloak.keycloak-core" />
<module name="org.keycloak.keycloak-server-spi" />
<module name="org.keycloak.keycloak-server-spi-private" />
<module name="org.keycloak.keycloak-kerberos-federation" />
<module name="org.keycloak.keycloak-ldap-federation" />
<module name="org.keycloak.keycloak-model-jpa" />
<module name="org.keycloak.keycloak-common" />
<module name="org.keycloak.keycloak-model-infinispan" />
<module name="org.keycloak.keycloak-services" />
</dependencies>
</deployment>
</jboss-deployment-structure>
1.1 Compile
I've only tested with a fat Jar in order to have external dependencies embedded in the jar. To do so, on maven, you run the following command:
Note that you must have the plugin for assembling.
$ mvn clean install assembly:single
- Deploying
This will show how to deploy into a running standalone Keycloak instance. If you are going to deploy keycloak on a docker or embedded system you will have to check how to deploy an SPI there.
Copy your fat jar into the standalone deployments folder:
${KEYCLOAK_HOME}/standalone/deployments/
if the keycloak is running, a jar.ISDEPLOYNG file will appear and a jar.DEPLOYED if everything went well.
this means that if you go to keycloak user federation page your custom Provider should appear
来源:https://stackoverflow.com/questions/64504639/how-to-create-a-custom-userstoragespi-on-keycloak