Как создать пользовательский UserStorageSPI в Keycloak

#java #maven #ldap #keycloak

#java #maven #ldap #keycloak

Вопрос:

Чтобы переопределить соединение LDAP и перенаправить проверку пароля в нашу собственную систему кэшированных паролей, в keycloak, всякий раз, когда соединение LDAP теряется. Более простым способом было создать HAProxy вокруг LDAP, чтобы гарантировать, что он никогда не выйдет из строя, но у нас нет доступа к этому, и наш клиент хочет перенаправить на нашу систему кэшированных паролей. Тем не менее, смысл этого поста в том, чтобы рассказать, как создать пользовательский поставщик хранилища LDAP для keycloak.

(проверьте документацию keycloak).

  1. Построение

Создайте Java-проект (jar) и добавьте следующие зависимости

pom.xml (для maven! при использовании gradle необходимо также добавить эти зависимости)

Примечание: убедитесь, что версия зависимостей keycloak совпадает с версией запущенного экземпляра keycloak, также обратите внимание, что эти зависимости имеют значение scope=provided, что означает, что они уже будут в загрузчике классов.

 <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>
  

Далее нам нужно будет добавить два класса, которые расширяют существующий поставщик LDAP, поскольку нам также понадобятся все эти функции, и просто нужно настроить несколько методов.

Пользовательский ldapstorageprovider расширит LDAPStorageProvider и будет выглядеть следующим образом:

Примечание: эти переопределенные методы предназначены для тестирования и отладки, здесь пока нет бизнес-логики, поскольку я буду добавлен позже по мере необходимости. Обратите внимание также, что в исключениях проверки пароля LDAP мы просто говорим, что действителен «истинный» пароль. Здесь мы вызовем наш собственный механизм проверки паролей.

 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. 

в src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory (создайте этот файл и добавьте следующую строку)

 foo.bar.lion.storage.ldap.CustomLDAPStorageProviderFactory
  

Нам также нужно добавить файл Jboss, чтобы указать, от чего мы зависим, поэтому мы создаем этот файл:

в src/main/resources/META-INF/jboss-deployment-structure.xml

Обратите внимание, что неправильные зависимости здесь могут привести к (Вызванному: «java.util.Ошибка ServiceConfigurationError»: org.keycloak.foo.bar: org.keycloak.foo.Abar не является подтипом)

 <?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 Компиляция

Я тестировал только с fat Jar, чтобы внешние зависимости были встроены в jar. Для этого в maven необходимо выполнить следующую команду:

Обратите внимание, что у вас должен быть плагин для сборки.

 $ mvn clean install assembly:single
  
  1. Развертывание

Здесь будет показано, как выполнить развертывание в работающем автономном экземпляре Keycloak. Если вы собираетесь развернуть keycloak в docker или встроенной системе, вам нужно будет проверить, как развернуть SPI там.

Скопируйте ваш fat jar в папку автономных развертываний:

$ {KEYCLOAK_HOME}/standalone/deployments/

если keycloak запущен, jar.Появится файл ISDEPLOYNG и jar.РАЗВЕРНУТО, если все прошло хорошо.

это означает, что если вы перейдете на страницу федерации пользователей keycloak, должен появиться ваш пользовательский поставщик

Комментарии:

1. Вы, наконец, решили проблему с HTML-метками?

2. Привет, извините, но я еще не понял. это был PoC, и не было необходимости разбираться в этом.

3. попробуйте вызвать super.getConfigProperties()