CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-keycloak--keycloak-server-spi

Service Provider Interface (SPI) contracts and abstractions for the Keycloak identity and access management server enabling extensibility through custom providers

Pending
Overview
Eval results
Files

user-storage.mddocs/

User Storage

The user storage framework provides interfaces for integrating external user stores with Keycloak. This enables federation with LDAP directories, databases, custom APIs, and other user repositories.

Core Storage Interfaces

UserStorageProvider

Base interface for all user storage providers.

public interface UserStorageProvider extends Provider {
    // Marker interface - specific capabilities are defined by extending interfaces
}

UserLookupProvider

Provides user lookup capabilities by ID, username, or email.

public interface UserLookupProvider {
    /**
     * Looks up a user by ID.
     * 
     * @param realm the realm
     * @param id the user ID
     * @return user model or null if not found
     */
    UserModel getUserById(RealmModel realm, String id);

    /**
     * Looks up a user by username.
     * 
     * @param realm the realm
     * @param username the username
     * @return user model or null if not found
     */
    UserModel getUserByUsername(RealmModel realm, String username);

    /**
     * Looks up a user by email address.
     * 
     * @param realm the realm
     * @param email the email address
     * @return user model or null if not found
     */
    UserModel getUserByEmail(RealmModel realm, String email);
}

UserQueryProvider

Provides user query and search capabilities.

public interface UserQueryProvider extends UserQueryMethodsProvider, UserCountMethodsProvider {
    // Inherited from UserQueryMethodsProvider:
    
    /**
     * Searches for users by search term.
     * 
     * @param realm the realm
     * @param search the search term
     * @return stream of matching users
     */
    Stream<UserModel> searchForUserStream(RealmModel realm, String search);

    /**
     * Searches for users by search term with pagination.
     * 
     * @param realm the realm
     * @param search the search term
     * @param firstResult first result index
     * @param maxResults maximum number of results
     * @return stream of matching users
     */
    Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);

    /**
     * Searches for users by attributes.
     * 
     * @param realm the realm
     * @param attributes search attributes map
     * @return stream of matching users
     */
    Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue);

    /**
     * Gets users in a specific group.
     * 
     * @param realm the realm
     * @param group the group
     * @return stream of group members
     */
    Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group);

    /**
     * Gets users in a specific group with pagination.
     * 
     * @param realm the realm
     * @param group the group
     * @param firstResult first result index
     * @param maxResults maximum number of results
     * @return stream of group members
     */
    Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults);

    /**
     * Searches for users by attributes with pagination.
     * 
     * @param realm the realm
     * @param attributes search attributes map
     * @param firstResult first result index
     * @param maxResults maximum number of results
     * @return stream of matching users
     */
    Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue, Integer firstResult, Integer maxResults);

    // Inherited from UserCountMethodsProvider:

    /**
     * Gets total number of users in the realm.
     * 
     * @param realm the realm
     * @return user count
     */
    int getUsersCount(RealmModel realm);

    /**
     * Gets number of users matching search criteria.
     * 
     * @param realm the realm
     * @param search the search term
     * @return matching user count
     */
    int getUsersCount(RealmModel realm, String search);

    /**
     * Gets number of users with specific attribute value.
     * 
     * @param realm the realm
     * @param attrName attribute name
     * @param attrValue attribute value
     * @return matching user count
     */
    int getUsersCount(RealmModel realm, String attrName, String attrValue);
}

UserRegistrationProvider

Enables user registration operations (create, update, delete).

public interface UserRegistrationProvider {
    /**
     * Adds a new user to the storage.
     * 
     * @param realm the realm
     * @param username the username
     * @return created user model
     */
    UserModel addUser(RealmModel realm, String username);

    /**
     * Removes a user from the storage.
     * 
     * @param realm the realm
     * @param user the user to remove
     * @return true if user was removed
     */
    boolean removeUser(RealmModel realm, UserModel user);
}

UserBulkUpdateProvider

Provides bulk user update capabilities.

public interface UserBulkUpdateProvider {
    /**
     * Disables all users in the realm.
     * 
     * @param realm the realm
     * @param includeServiceAccount whether to include service accounts
     */
    void disableUsers(RealmModel realm, boolean includeServiceAccount);

    /**
     * Removes imported users (federated users) from local storage.
     * 
     * @param realm the realm
     * @param storageProviderId the storage provider ID
     */
    void removeImportedUsers(RealmModel realm, String storageProviderId);

    /**
     * Removes expired users based on configuration.
     * 
     * @param realm the realm
     */
    void removeExpiredUsers(RealmModel realm);
}

Storage Utilities

StorageId

Utility class for handling federated storage IDs.

public class StorageId {
    private final String storageProviderId;
    private final String externalId;

    /**
     * Creates a StorageId from provider ID and external ID.
     * 
     * @param storageProviderId the storage provider ID
     * @param externalId the external ID
     */
    public StorageId(String storageProviderId, String externalId) {
        this.storageProviderId = storageProviderId;
        this.externalId = externalId;
    }

    /**
     * Parses a storage ID string.
     * 
     * @param id the storage ID string
     * @return parsed StorageId
     */
    public static StorageId resolveId(String id) {
        // Implementation parses "f:{providerId}:{externalId}" format
    }

    /**
     * Checks if an ID is a federated/external ID.
     * 
     * @param id the ID to check
     * @return true if federated ID
     */
    public static boolean isLocalId(String id) {
        return id != null && !id.startsWith("f:");
    }

    /**
     * Gets the storage provider ID.
     * 
     * @return storage provider ID
     */
    public String getProviderId() {
        return storageProviderId;
    }

    /**
     * Gets the external ID.
     * 
     * @return external ID
     */
    public String getExternalId() {
        return externalId;
    }

    /**
     * Gets the full keycloak ID.
     * 
     * @return full ID in format "f:{providerId}:{externalId}"
     */
    public String getId() {
        return "f:" + storageProviderId + ":" + externalId;
    }
}

SynchronizationResult

Result object for user synchronization operations.

public class SynchronizationResult {
    private boolean ignored = false;
    private int added = 0;
    private int updated = 0;
    private int removed = 0;
    private int failed = 0;

    public static SynchronizationResult empty() {
        return new SynchronizationResult();
    }

    public static SynchronizationResult ignored() {
        SynchronizationResult result = new SynchronizationResult();
        result.ignored = true;
        return result;
    }

    // Getters and setters
    public boolean isIgnored() { return ignored; }
    public void setIgnored(boolean ignored) { this.ignored = ignored; }

    public int getAdded() { return added; }
    public void setAdded(int added) { this.added = added; }
    public void increaseAdded() { this.added++; }

    public int getUpdated() { return updated; }
    public void setUpdated(int updated) { this.updated = updated; }
    public void increaseUpdated() { this.updated++; }

    public int getRemoved() { return removed; }
    public void setRemoved(int removed) { this.removed = removed; }
    public void increaseRemoved() { this.removed++; }

    public int getFailed() { return failed; }
    public void setFailed(int failed) { this.failed = failed; }
    public void increaseFailed() { this.failed++; }

    public void add(SynchronizationResult other) {
        this.added += other.added;
        this.updated += other.updated;
        this.removed += other.removed;
        this.failed += other.failed;
    }

    @Override
    public String toString() {
        return String.format("SynchronizationResult [added=%d, updated=%d, removed=%d, failed=%d, ignored=%s]",
                added, updated, removed, failed, ignored);
    }
}

Storage Lookup Providers

ClientLookupProvider

Provides client lookup capabilities for federated client storage.

public interface ClientLookupProvider {
    /**
     * Looks up a client by ID.
     * 
     * @param realm the realm
     * @param id the client ID
     * @return client model or null if not found
     */
    ClientModel getClientById(RealmModel realm, String id);

    /**
     * Looks up a client by client ID.
     * 
     * @param realm the realm
     * @param clientId the client ID string
     * @return client model or null if not found
     */
    ClientModel getClientByClientId(RealmModel realm, String clientId);

    /**
     * Searches for clients by client ID.
     * 
     * @param realm the realm
     * @param clientId the client ID pattern
     * @param firstResult first result index
     * @param maxResults max results
     * @return stream of matching clients
     */
    Stream<ClientModel> searchClientsByClientIdStream(RealmModel realm, String clientId, Integer firstResult, Integer maxResults);

    /**
     * Searches for clients by attributes.
     * 
     * @param realm the realm
     * @param attributes search attributes
     * @param firstResult first result index
     * @param maxResults max results
     * @return stream of matching clients
     */
    Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults);

    /**
     * Gets all clients in the realm.
     * 
     * @param realm the realm
     * @param firstResult first result index
     * @param maxResults max results
     * @return stream of clients
     */
    Stream<ClientModel> getClientsStream(RealmModel realm, Integer firstResult, Integer maxResults);
}

GroupLookupProvider

Provides group lookup capabilities for federated group storage.

public interface GroupLookupProvider {
    /**
     * Looks up a group by ID.
     * 
     * @param realm the realm
     * @param id the group ID
     * @return group model or null if not found
     */
    GroupModel getGroupById(RealmModel realm, String id);

    /**
     * Searches for groups by name.
     * 
     * @param realm the realm
     * @param search the search term
     * @param firstResult first result index
     * @param maxResults max results
     * @return stream of matching groups
     */
    Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
}

RoleLookupProvider

Provides role lookup capabilities for federated role storage.

public interface RoleLookupProvider {
    /**
     * Looks up a role by name.
     * 
     * @param realm the realm
     * @param name the role name
     * @return role model or null if not found
     */
    RoleModel getRealmRole(RealmModel realm, String name);

    /**
     * Looks up a client role by name.
     * 
     * @param realm the realm
     * @param client the client
     * @param name the role name
     * @return role model or null if not found
     */
    RoleModel getClientRole(RealmModel realm, ClientModel client, String name);

    /**
     * Searches for roles by name.
     * 
     * @param realm the realm
     * @param search the search term
     * @param firstResult first result index
     * @param maxResults max results
     * @return stream of matching roles
     */
    Stream<RoleModel> searchForRolesStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
}

Exception Handling

ReadOnlyException

Exception thrown when attempting write operations on read-only storage.

public class ReadOnlyException extends RuntimeException {
    public ReadOnlyException(String message) {
        super(message);
    }

    public ReadOnlyException(String message, Throwable cause) {
        super(message, cause);
    }
}

Usage Examples

Creating a Custom User Storage Provider

public class LdapUserStorageProvider implements UserStorageProvider, 
                                                UserLookupProvider, 
                                                UserQueryProvider, 
                                                CredentialInputValidator {
    
    private final KeycloakSession session;
    private final ComponentModel model;
    private final LdapConnectionManager connectionManager;

    public LdapUserStorageProvider(KeycloakSession session, ComponentModel model) {
        this.session = session;
        this.model = model;
        this.connectionManager = new LdapConnectionManager(model);
    }

    @Override
    public UserModel getUserById(RealmModel realm, String id) {
        StorageId storageId = new StorageId(id);
        if (!storageId.getProviderId().equals(model.getId())) {
            return null;
        }
        return getUserByLdapDn(realm, storageId.getExternalId());
    }

    @Override
    public UserModel getUserByUsername(RealmModel realm, String username) {
        try (LdapContext context = connectionManager.getLdapContext()) {
            String dn = findUserDnByUsername(context, username);
            if (dn != null) {
                return createUserModel(realm, dn, username);
            }
        } catch (Exception e) {
            logger.error("LDAP search failed", e);
        }
        return null;
    }

    @Override
    public UserModel getUserByEmail(RealmModel realm, String email) {
        try (LdapContext context = connectionManager.getLdapContext()) {
            String dn = findUserDnByEmail(context, email);
            if (dn != null) {
                return createUserModel(realm, dn, extractUsername(dn));
            }
        } catch (Exception e) {
            logger.error("LDAP search failed", e);
        }
        return null;
    }

    @Override
    public Stream<UserModel> searchForUserStream(RealmModel realm, String search, 
                                                Integer firstResult, Integer maxResults) {
        try (LdapContext context = connectionManager.getLdapContext()) {
            List<String> userDns = searchUsers(context, search, firstResult, maxResults);
            return userDns.stream()
                         .map(dn -> createUserModel(realm, dn, extractUsername(dn)))
                         .filter(Objects::nonNull);
        } catch (Exception e) {
            logger.error("LDAP search failed", e);
            return Stream.empty();
        }
    }

    @Override
    public int getUsersCount(RealmModel realm) {
        try (LdapContext context = connectionManager.getLdapContext()) {
            return countUsers(context);
        } catch (Exception e) {
            logger.error("LDAP count failed", e);
            return 0;
        }
    }

    @Override
    public boolean supportsCredentialType(String credentialType) {
        return PasswordCredentialModel.TYPE.equals(credentialType);
    }

    @Override
    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
        return supportsCredentialType(credentialType);
    }

    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
        if (!supportsCredentialType(input.getType())) {
            return false;
        }
        
        String username = user.getUsername();
        String password = input.getChallengeResponse();
        
        try (LdapContext context = connectionManager.getLdapContext(username, password)) {
            return context != null;
        } catch (Exception e) {
            logger.debug("LDAP authentication failed for user " + username, e);
            return false;
        }
    }

    @Override
    public void close() {
        if (connectionManager != null) {
            connectionManager.close();
        }
    }

    private UserModel createUserModel(RealmModel realm, String ldapDn, String username) {
        String id = new StorageId(model.getId(), ldapDn).getId();
        return new LdapUserModel(session, realm, model, id, username, ldapDn);
    }
}

Creating the User Storage Provider Factory

public class LdapUserStorageProviderFactory implements UserStorageProviderFactory<LdapUserStorageProvider> {
    public static final String PROVIDER_ID = "ldap";

    @Override
    public LdapUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new LdapUserStorageProvider(session, model);
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return Arrays.asList(
            new ProviderConfigProperty("ldap.host", "LDAP Host", "LDAP server hostname", 
                                       ProviderConfigProperty.STRING_TYPE, "localhost"),
            new ProviderConfigProperty("ldap.port", "LDAP Port", "LDAP server port", 
                                       ProviderConfigProperty.STRING_TYPE, "389"),
            new ProviderConfigProperty("ldap.baseDn", "Base DN", "Base DN for user searches", 
                                       ProviderConfigProperty.STRING_TYPE, "ou=users,dc=example,dc=com"),
            new ProviderConfigProperty("ldap.bindDn", "Bind DN", "DN for LDAP binding", 
                                       ProviderConfigProperty.STRING_TYPE, null),
            new ProviderConfigProperty("ldap.bindPassword", "Bind Password", "Password for LDAP binding", 
                                       ProviderConfigProperty.PASSWORD, null)
        );
    }

    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) 
            throws ComponentValidationException {
        String host = config.get("ldap.host");
        if (host == null || host.trim().isEmpty()) {
            throw new ComponentValidationException("LDAP host is required");
        }

        String baseDn = config.get("ldap.baseDn");
        if (baseDn == null || baseDn.trim().isEmpty()) {
            throw new ComponentValidationException("Base DN is required");
        }

        // Test LDAP connection
        try {
            testLdapConnection(config);
        } catch (Exception e) {
            throw new ComponentValidationException("Failed to connect to LDAP server: " + e.getMessage());
        }
    }

    private void testLdapConnection(ComponentModel config) throws Exception {
        // Implementation to test LDAP connection
    }
}

Implementing a Read-Only User Model

public class LdapUserModel implements UserModel {
    private final KeycloakSession session;
    private final RealmModel realm;
    private final ComponentModel storageProvider;
    private final String id;
    private final String username;
    private final String ldapDn;
    private final Map<String, List<String>> attributes = new HashMap<>();

    public LdapUserModel(KeycloakSession session, RealmModel realm, ComponentModel storageProvider,
                        String id, String username, String ldapDn) {
        this.session = session;
        this.realm = realm;
        this.storageProvider = storageProvider;
        this.id = id;
        this.username = username;
        this.ldapDn = ldapDn;
        
        // Load attributes from LDAP
        loadAttributesFromLdap();
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public void setUsername(String username) {
        throw new ReadOnlyException("User is read-only");
    }

    @Override
    public String getFirstName() {
        return getFirstAttribute("givenName");
    }

    @Override
    public void setFirstName(String firstName) {
        throw new ReadOnlyException("User is read-only");
    }

    @Override
    public String getLastName() {
        return getFirstAttribute("sn");
    }

    @Override
    public void setLastName(String lastName) {
        throw new ReadOnlyException("User is read-only");
    }

    @Override
    public String getEmail() {
        return getFirstAttribute("mail");
    }

    @Override
    public void setEmail(String email) {
        throw new ReadOnlyException("User is read-only");
    }

    @Override
    public boolean isEnabled() {
        return true; // Assume LDAP users are enabled
    }

    @Override
    public void setEnabled(boolean enabled) {
        throw new ReadOnlyException("User is read-only");
    }

    @Override
    public Map<String, List<String>> getAttributes() {
        return attributes;
    }

    @Override
    public String getFirstAttribute(String name) {
        List<String> values = attributes.get(name);
        return values != null && !values.isEmpty() ? values.get(0) : null;
    }

    @Override
    public Stream<String> getAttributeStream(String name) {
        List<String> values = attributes.get(name);
        return values != null ? values.stream() : Stream.empty();
    }

    @Override
    public void setSingleAttribute(String name, String value) {
        throw new ReadOnlyException("User is read-only");
    }

    @Override
    public void setAttribute(String name, List<String> values) {
        throw new ReadOnlyException("User is read-only");
    }

    @Override
    public void removeAttribute(String name) {
        throw new ReadOnlyException("User is read-only");
    }

    private void loadAttributesFromLdap() {
        // Implementation to load attributes from LDAP
    }

    // Implement other UserModel methods...
}

Using Storage Providers

// Access federated users
try (KeycloakSession session = sessionFactory.create()) {
    RealmModel realm = session.realms().getRealmByName("myrealm");
    
    // Get user from any storage (local or federated)
    UserModel user = session.users().getUserByUsername(realm, "john.doe");
    
    // Check if user is from federated storage
    if (!StorageId.isLocalId(user.getId())) {
        StorageId storageId = new StorageId(user.getId());
        String providerId = storageId.getProviderId();
        String externalId = storageId.getExternalId();
        
        System.out.println("User is from storage provider: " + providerId);
        System.out.println("External ID: " + externalId);
    }
    
    // Search across all storage providers
    Stream<UserModel> users = session.users()
        .searchForUserStream(realm, "john", 0, 10);
}

Install with Tessl CLI

npx tessl i tessl/maven-org-keycloak--keycloak-server-spi

docs

authentication-sessions.md

component-framework.md

core-models.md

credential-management.md

index.md

organization-management.md

provider-framework.md

session-management.md

user-storage.md

validation-framework.md

vault-integration.md

tile.json