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

component-framework.mddocs/

Component Framework

The component framework provides a configuration-driven approach to managing provider instances in Keycloak. It enables dynamic configuration of providers with validation, lifecycle management, and hierarchical organization.

Core Component Interfaces

ComponentFactory

Factory interface for component-based providers with configuration support.

public interface ComponentFactory<T extends Provider> extends ProviderFactory<T> {
    /**
     * Creates a provider instance with the given component configuration.
     * 
     * @param session the Keycloak session
     * @param model the component model with configuration
     * @return provider instance
     */
    T create(KeycloakSession session, ComponentModel model);

    /**
     * Validates the component configuration.
     * 
     * @param session the Keycloak session
     * @param realm the realm
     * @param model the component model to validate
     * @throws ComponentValidationException if validation fails
     */
    void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) 
            throws ComponentValidationException;

    /**
     * Called when a component instance is created.
     * 
     * @param session the Keycloak session
     * @param realm the realm
     * @param model the component model
     */
    void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model);

    /**
     * Called when a component configuration is updated.
     * 
     * @param session the Keycloak session
     * @param realm the realm
     * @param oldModel the previous component model
     * @param newModel the updated component model
     */
    void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel);

    /**
     * Gets configuration properties for this component type.
     * 
     * @return list of configuration properties
     */
    @Override
    default List<ProviderConfigProperty> getConfigMetadata() {
        return Collections.emptyList();
    }

    /**
     * Gets configuration properties specific to the component context.
     * 
     * @param session the Keycloak session
     * @param realm the realm
     * @return list of configuration properties
     */
    default List<ProviderConfigProperty> getConfigProperties(KeycloakSession session, RealmModel realm) {
        return getConfigMetadata();
    }
}

SubComponentFactory

Factory for components that can have sub-components.

public interface SubComponentFactory<T extends Provider, S extends Provider> extends ComponentFactory<T> {
    /**
     * Gets the provider class for sub-components.
     * 
     * @return sub-component provider class
     */
    Class<S> getSubComponentClass();

    /**
     * Gets supported sub-component types.
     * 
     * @param session the Keycloak session
     * @param realm the realm
     * @param model the parent component model
     * @return list of supported sub-component types
     */
    List<String> getSubComponentTypes(KeycloakSession session, RealmModel realm, ComponentModel model);

    /**
     * Gets configuration properties for a sub-component type.
     * 
     * @param session the Keycloak session
     * @param realm the realm
     * @param parent the parent component model
     * @param subType the sub-component type
     * @return list of configuration properties
     */
    List<ProviderConfigProperty> getSubComponentConfigProperties(KeycloakSession session, RealmModel realm, 
                                                                ComponentModel parent, String subType);
}

Component Model

ComponentModel

Model representing a component configuration.

public class ComponentModel {
    private String id;
    private String name;
    private String providerId;
    private String providerType;
    private String parentId;
    private String subType;
    private MultivaluedHashMap<String, String> config;

    // Constructors
    public ComponentModel() {
        this.config = new MultivaluedHashMap<>();
    }

    public ComponentModel(ComponentModel copy) {
        this.id = copy.id;
        this.name = copy.name;
        this.providerId = copy.providerId;
        this.providerType = copy.providerType;
        this.parentId = copy.parentId;
        this.subType = copy.subType;
        this.config = new MultivaluedHashMap<>(copy.config);
    }

    public ComponentModel(String id, String name, String parentId, String providerId, String providerType) {
        this();
        this.id = id;
        this.name = name;
        this.parentId = parentId;
        this.providerId = providerId;
        this.providerType = providerType;
    }

    // Getters and setters
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getProviderId() { return providerId; }
    public void setProviderId(String providerId) { this.providerId = providerId; }

    public String getProviderType() { return providerType; }
    public void setProviderType(String providerType) { this.providerType = providerType; }

    public String getParentId() { return parentId; }
    public void setParentId(String parentId) { this.parentId = parentId; }

    public String getSubType() { return subType; }
    public void setSubType(String subType) { this.subType = subType; }

    public MultivaluedHashMap<String, String> getConfig() { return config; }
    public void setConfig(MultivaluedHashMap<String, String> config) { this.config = config; }

    // Configuration helper methods
    public String get(String key) {
        List<String> values = config.get(key);
        return values != null && !values.isEmpty() ? values.get(0) : null;
    }

    public String get(String key, String defaultValue) {
        String value = get(key);
        return value != null ? value : defaultValue;
    }

    public List<String> getList(String key) {
        List<String> values = config.get(key);
        return values != null ? values : Collections.emptyList();
    }

    public int get(String key, int defaultValue) {
        String value = get(key);
        if (value == null) return defaultValue;
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    public long get(String key, long defaultValue) {
        String value = get(key);
        if (value == null) return defaultValue;
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    public boolean get(String key, boolean defaultValue) {
        String value = get(key);
        return value != null ? Boolean.parseBoolean(value) : defaultValue;
    }

    public void put(String key, String value) {
        if (value == null) {
            config.remove(key);
        } else {
            config.putSingle(key, value);
        }
    }

    public void put(String key, List<String> values) {
        if (values == null || values.isEmpty()) {
            config.remove(key);
        } else {
            config.put(key, values);
        }
    }

    public void put(String key, int value) {
        put(key, String.valueOf(value));
    }

    public void put(String key, long value) {
        put(key, String.valueOf(value));
    }

    public void put(String key, boolean value) {
        put(key, String.valueOf(value));
    }

    public boolean contains(String key) {
        return config.containsKey(key);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        ComponentModel that = (ComponentModel) obj;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return String.format("ComponentModel{id='%s', name='%s', providerId='%s', providerType='%s'}", 
                           id, name, providerId, providerType);
    }
}

ConfiguredComponent

Interface for components that require configuration.

public interface ConfiguredComponent {
    /**
     * Gets the component model configuration.
     * 
     * @return component model
     */
    ComponentModel getModel();
}

PrioritizedComponentModel

Component model with priority ordering.

public class PrioritizedComponentModel extends ComponentModel {
    private int priority;

    public PrioritizedComponentModel() {
        super();
    }

    public PrioritizedComponentModel(ComponentModel copy) {
        super(copy);
        if (copy instanceof PrioritizedComponentModel) {
            this.priority = ((PrioritizedComponentModel) copy).priority;
        }
    }

    public PrioritizedComponentModel(ComponentModel copy, int priority) {
        super(copy);
        this.priority = priority;
    }

    public int getPriority() { return priority; }
    public void setPriority(int priority) { this.priority = priority; }

    @Override
    public String toString() {
        return String.format("PrioritizedComponentModel{id='%s', name='%s', providerId='%s', priority=%d}", 
                           getId(), getName(), getProviderId(), priority);
    }
}

JsonConfigComponentModel

Component model that stores configuration as JSON.

public class JsonConfigComponentModel extends ComponentModel {
    private Map<String, Object> jsonConfig;

    public JsonConfigComponentModel() {
        super();
        this.jsonConfig = new HashMap<>();
    }

    public JsonConfigComponentModel(ComponentModel copy) {
        super(copy);
        this.jsonConfig = new HashMap<>();
    }

    public Map<String, Object> getJsonConfig() { return jsonConfig; }
    public void setJsonConfig(Map<String, Object> jsonConfig) { this.jsonConfig = jsonConfig; }

    public <T> T getJsonConfig(String key, Class<T> type) {
        Object value = jsonConfig.get(key);
        return type.isInstance(value) ? type.cast(value) : null;
    }

    public void putJsonConfig(String key, Object value) {
        if (value == null) {
            jsonConfig.remove(key);
        } else {
            jsonConfig.put(key, value);
        }
    }
}

Exception Handling

ComponentValidationException

Exception thrown during component validation.

public class ComponentValidationException extends Exception {
    private final Object[] parameters;

    public ComponentValidationException(String message) {
        super(message);
        this.parameters = null;
    }

    public ComponentValidationException(String message, Object... parameters) {
        super(message);
        this.parameters = parameters;
    }

    public ComponentValidationException(String message, Throwable cause) {
        super(message, cause);
        this.parameters = null;
    }

    public ComponentValidationException(String message, Throwable cause, Object... parameters) {
        super(message, cause);
        this.parameters = parameters;
    }

    public Object[] getParameters() { return parameters; }

    public String getLocalizedMessage() {
        if (parameters != null && parameters.length > 0) {
            return String.format(getMessage(), parameters);
        }
        return getMessage();
    }
}

Usage Examples

Creating a Component Factory

public class EmailProviderFactory implements ComponentFactory<EmailProvider> {
    public static final String PROVIDER_ID = "smtp-email";

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

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

    @Override
    public List<ProviderConfigProperty> getConfigMetadata() {
        return Arrays.asList(
            new ProviderConfigProperty("host", "SMTP Host", "SMTP server hostname", 
                                       ProviderConfigProperty.STRING_TYPE, "localhost"),
            new ProviderConfigProperty("port", "SMTP Port", "SMTP server port", 
                                       ProviderConfigProperty.STRING_TYPE, "587"),
            new ProviderConfigProperty("username", "Username", "SMTP username", 
                                       ProviderConfigProperty.STRING_TYPE, null),
            new ProviderConfigProperty("password", "Password", "SMTP password", 
                                       ProviderConfigProperty.PASSWORD, null),
            new ProviderConfigProperty("tls", "Enable TLS", "Enable TLS encryption", 
                                       ProviderConfigProperty.BOOLEAN_TYPE, true),
            new ProviderConfigProperty("from", "From Address", "Email from address", 
                                       ProviderConfigProperty.STRING_TYPE, "noreply@example.com")
        );
    }

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

        String port = model.get("port");
        if (port != null && !port.isEmpty()) {
            try {
                int portNum = Integer.parseInt(port);
                if (portNum < 1 || portNum > 65535) {
                    throw new ComponentValidationException("Port must be between 1 and 65535");
                }
            } catch (NumberFormatException e) {
                throw new ComponentValidationException("Invalid port number: " + port);
            }
        }

        String fromAddress = model.get("from");
        if (fromAddress == null || !fromAddress.contains("@")) {
            throw new ComponentValidationException("Valid from address is required");
        }

        // Test SMTP connection
        try {
            testSmtpConnection(model);
        } catch (Exception e) {
            throw new ComponentValidationException("Failed to connect to SMTP server: " + e.getMessage(), e);
        }
    }

    @Override
    public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
        // Log component creation
        logger.info("Created email provider component: " + model.getName());
    }

    @Override
    public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
        // Handle configuration changes
        if (!Objects.equals(oldModel.get("host"), newModel.get("host"))) {
            logger.info("Email provider host changed from {} to {}", 
                       oldModel.get("host"), newModel.get("host"));
        }
    }

    private void testSmtpConnection(ComponentModel model) throws Exception {
        // Implementation to test SMTP connection
        Properties props = new Properties();
        props.put("mail.smtp.host", model.get("host"));
        props.put("mail.smtp.port", model.get("port", "587"));
        props.put("mail.smtp.starttls.enable", model.get("tls", true));
        
        String username = model.get("username");
        String password = model.get("password");
        
        Session mailSession;
        if (username != null && password != null) {
            props.put("mail.smtp.auth", "true");
            mailSession = Session.getInstance(props, new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(username, password);
                }
            });
        } else {
            mailSession = Session.getInstance(props);
        }
        
        Transport transport = mailSession.getTransport("smtp");
        transport.connect();
        transport.close();
    }
}

Creating a Sub-Component Factory

public class LdapProviderFactory implements SubComponentFactory<UserStorageProvider, UserStorageProviderModel> {
    public static final String PROVIDER_ID = "ldap";

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

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

    @Override
    public Class<UserStorageProviderModel> getSubComponentClass() {
        return UserStorageProviderModel.class;
    }

    @Override
    public List<String> getSubComponentTypes(KeycloakSession session, RealmModel realm, ComponentModel model) {
        return Arrays.asList("attribute-mapper", "group-mapper", "role-mapper");
    }

    @Override
    public List<ProviderConfigProperty> getSubComponentConfigProperties(KeycloakSession session, RealmModel realm, 
                                                                       ComponentModel parent, String subType) {
        switch (subType) {
            case "attribute-mapper":
                return getAttributeMapperProperties();
            case "group-mapper":
                return getGroupMapperProperties();
            case "role-mapper":
                return getRoleMapperProperties();
            default:
                return Collections.emptyList();
        }
    }

    @Override
    public List<ProviderConfigProperty> getConfigMetadata() {
        return Arrays.asList(
            new ProviderConfigProperty("connectionUrl", "Connection URL", "LDAP connection URL", 
                                       ProviderConfigProperty.STRING_TYPE, "ldap://localhost:389"),
            new ProviderConfigProperty("bindDn", "Bind DN", "DN of LDAP admin user", 
                                       ProviderConfigProperty.STRING_TYPE, null),
            new ProviderConfigProperty("bindCredential", "Bind Credential", "Password of LDAP admin user", 
                                       ProviderConfigProperty.PASSWORD, null),
            new ProviderConfigProperty("usersDn", "Users DN", "DN where users are stored", 
                                       ProviderConfigProperty.STRING_TYPE, "ou=users,dc=example,dc=com"),
            new ProviderConfigProperty("usernameAttribute", "Username Attribute", "LDAP attribute for username", 
                                       ProviderConfigProperty.STRING_TYPE, "uid")
        );
    }

    private List<ProviderConfigProperty> getAttributeMapperProperties() {
        return Arrays.asList(
            new ProviderConfigProperty("ldap.attribute", "LDAP Attribute", "LDAP attribute name", 
                                       ProviderConfigProperty.STRING_TYPE, null),
            new ProviderConfigProperty("user.model.attribute", "User Model Attribute", "User model attribute name", 
                                       ProviderConfigProperty.STRING_TYPE, null),
            new ProviderConfigProperty("read.only", "Read Only", "Whether attribute is read-only", 
                                       ProviderConfigProperty.BOOLEAN_TYPE, false)
        );
    }

    private List<ProviderConfigProperty> getGroupMapperProperties() {
        return Arrays.asList(
            new ProviderConfigProperty("groups.dn", "Groups DN", "DN where groups are stored", 
                                       ProviderConfigProperty.STRING_TYPE, "ou=groups,dc=example,dc=com"),
            new ProviderConfigProperty("group.name.attribute", "Group Name Attribute", "LDAP attribute for group name", 
                                       ProviderConfigProperty.STRING_TYPE, "cn"),
            new ProviderConfigProperty("membership.attribute", "Membership Attribute", "LDAP attribute for membership", 
                                       ProviderConfigProperty.STRING_TYPE, "member")
        );
    }

    private List<ProviderConfigProperty> getRoleMapperProperties() {
        return Arrays.asList(
            new ProviderConfigProperty("roles.dn", "Roles DN", "DN where roles are stored", 
                                       ProviderConfigProperty.STRING_TYPE, "ou=roles,dc=example,dc=com"),
            new ProviderConfigProperty("role.name.attribute", "Role Name Attribute", "LDAP attribute for role name", 
                                       ProviderConfigProperty.STRING_TYPE, "cn"),
            new ProviderConfigProperty("use.realm.roles.mapping", "Use Realm Roles", "Map to realm roles", 
                                       ProviderConfigProperty.BOOLEAN_TYPE, true)
        );
    }
}

Working with Components

// Create and configure a component
try (KeycloakSession session = sessionFactory.create()) {
    RealmModel realm = session.realms().getRealmByName("myrealm");
    
    // Create email provider component
    ComponentModel emailComponent = new ComponentModel();
    emailComponent.setName("SMTP Email Provider");
    emailComponent.setProviderId("smtp-email");
    emailComponent.setProviderType("email");
    emailComponent.setParentId(realm.getId());
    
    // Configure SMTP settings
    emailComponent.put("host", "smtp.gmail.com");
    emailComponent.put("port", "587");
    emailComponent.put("username", "myapp@gmail.com");
    emailComponent.put("password", "app-password");
    emailComponent.put("tls", true);
    emailComponent.put("from", "myapp@gmail.com");
    
    // Add the component to the realm
    realm.addComponent(emailComponent);
    
    // Get the email provider instance
    EmailProvider emailProvider = session.getProvider(EmailProvider.class, emailComponent.getId());
    
    // Use the provider
    emailProvider.send("test@example.com", "Test Subject", "Test message");
}

Component Hierarchy Management

// Working with component hierarchies
try (KeycloakSession session = sessionFactory.create()) {
    RealmModel realm = session.realms().getRealmByName("myrealm");
    
    // Create parent component (LDAP provider)
    ComponentModel ldapComponent = new ComponentModel();
    ldapComponent.setName("LDAP User Storage");
    ldapComponent.setProviderId("ldap");
    ldapComponent.setProviderType("org.keycloak.storage.UserStorageProvider");
    ldapComponent.setParentId(realm.getId());
    
    // Configure LDAP settings
    ldapComponent.put("connectionUrl", "ldap://ldap.example.com:389");
    ldapComponent.put("bindDn", "cn=admin,dc=example,dc=com");
    ldapComponent.put("bindCredential", "admin123");
    ldapComponent.put("usersDn", "ou=users,dc=example,dc=com");
    
    realm.addComponent(ldapComponent);
    
    // Create sub-component (attribute mapper)
    ComponentModel attributeMapper = new ComponentModel();
    attributeMapper.setName("Email Attribute Mapper");
    attributeMapper.setProviderId("user-attribute-ldap-mapper");
    attributeMapper.setProviderType("org.keycloak.storage.ldap.mappers.LDAPStorageMapper");
    attributeMapper.setParentId(ldapComponent.getId());
    attributeMapper.setSubType("attribute-mapper");
    
    // Configure attribute mapping
    attributeMapper.put("ldap.attribute", "mail");
    attributeMapper.put("user.model.attribute", "email");
    attributeMapper.put("read.only", false);
    
    realm.addComponent(attributeMapper);
    
    // Get all sub-components of the LDAP provider
    Stream<ComponentModel> subComponents = realm.getComponentsStream(ldapComponent.getId());
    subComponents.forEach(comp -> {
        System.out.println("Sub-component: " + comp.getName() + " (" + comp.getSubType() + ")");
    });
}

Component Configuration Validation

// Custom component with validation
public class CustomComponentFactory implements ComponentFactory<CustomProvider> {
    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) 
            throws ComponentValidationException {
        
        // Validate required fields
        validateRequired(model, "apiKey", "API Key is required");
        validateRequired(model, "endpoint", "Endpoint URL is required");
        
        // Validate URL format
        String endpoint = model.get("endpoint");
        try {
            new URL(endpoint);
        } catch (MalformedURLException e) {
            throw new ComponentValidationException("Invalid endpoint URL: " + endpoint);
        }
        
        // Validate numeric fields
        validateNumeric(model, "timeout", 1, 300, "Timeout must be between 1 and 300 seconds");
        validateNumeric(model, "maxRetries", 0, 10, "Max retries must be between 0 and 10");
        
        // Validate enum values
        String protocol = model.get("protocol");
        if (protocol != null && !Arrays.asList("http", "https").contains(protocol.toLowerCase())) {
            throw new ComponentValidationException("Protocol must be 'http' or 'https'");
        }
        
        // Custom business logic validation
        validateBusinessRules(session, realm, model);
    }
    
    private void validateRequired(ComponentModel model, String key, String errorMessage) 
            throws ComponentValidationException {
        String value = model.get(key);
        if (value == null || value.trim().isEmpty()) {
            throw new ComponentValidationException(errorMessage);
        }
    }
    
    private void validateNumeric(ComponentModel model, String key, int min, int max, String errorMessage) 
            throws ComponentValidationException {
        String value = model.get(key);
        if (value != null && !value.isEmpty()) {
            try {
                int intValue = Integer.parseInt(value);
                if (intValue < min || intValue > max) {
                    throw new ComponentValidationException(errorMessage);
                }
            } catch (NumberFormatException e) {
                throw new ComponentValidationException("Invalid number format for " + key + ": " + value);
            }
        }
    }
    
    private void validateBusinessRules(KeycloakSession session, RealmModel realm, ComponentModel model) 
            throws ComponentValidationException {
        // Custom validation logic specific to your component
        String apiKey = model.get("apiKey");
        if (apiKey != null && apiKey.length() < 32) {
            throw new ComponentValidationException("API key must be at least 32 characters long");
        }
        
        // Check for conflicts with existing components
        realm.getComponentsStream(realm.getId(), "custom-provider")
             .filter(comp -> !comp.getId().equals(model.getId()))
             .filter(comp -> comp.get("endpoint").equals(model.get("endpoint")))
             .findFirst()
             .ifPresent(existing -> {
                 throw new RuntimeException(new ComponentValidationException(
                     "Another component is already using this endpoint: " + existing.getName()));
             });
    }
}

Install with Tessl CLI

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

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