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

validation-framework.mddocs/

Validation Framework

The validation framework provides extensible field validation for user input, form data, and configuration parameters. It supports built-in validators and custom validation logic.

Core Validation Interfaces

Validator

Base interface for field validators.

public interface Validator {
    /**
     * Gets the validator ID.
     * 
     * @return validator ID
     */
    String getId();

    /**
     * Validates input value.
     * 
     * @param input the input value to validate
     * @param inputHint hint about the input context
     * @param context validation context
     * @param config validator configuration
     * @return validation result
     * @throws ValidationException if validation fails unexpectedly
     */
    ValidationResult validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) 
            throws ValidationException;
}

SimpleValidator

Base interface for simple validators that validate individual values.

public interface SimpleValidator extends Validator {
    /**
     * Validates a single input value.
     * 
     * @param input the input value
     * @param inputHint hint about the input
     * @param context validation context
     * @param config validator configuration
     * @return validation result
     */
    ValidationResult validateValue(Object input, String inputHint, ValidationContext context, ValidatorConfig config);

    @Override
    default ValidationResult validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) 
            throws ValidationException {
        return validateValue(input, inputHint, context, config);
    }
}

Validation Context

ValidationContext

Provides context for validation operations.

public interface ValidationContext extends AttributeContext {
    /**
     * Gets the Keycloak session.
     * 
     * @return Keycloak session
     */
    KeycloakSession getSession();

    /**
     * Gets the attributes being validated.
     * 
     * @return attributes
     */
    Attributes getAttributes();

    /**
     * Gets the current realm.
     * 
     * @return realm model
     */
    RealmModel getRealm();

    /**
     * Gets the current user (if available).
     * 
     * @return user model or null
     */
    UserModel getUser();
}

AttributeContext

Base context interface for attribute operations.

public interface AttributeContext {
    /**
     * Gets the Keycloak session.
     * 
     * @return Keycloak session
     */
    KeycloakSession getSession();

    /**
     * Gets the current realm.
     * 
     * @return realm model
     */
    RealmModel getRealm();
}

Validation Results

ValidationResult

Represents the result of a validation operation.

public class ValidationResult {
    public static final ValidationResult VALID = new ValidationResult();

    private final boolean valid;
    private final Set<ValidationError> errors;

    private ValidationResult() {
        this.valid = true;
        this.errors = Collections.emptySet();
    }

    private ValidationResult(Set<ValidationError> errors) {
        this.valid = false;
        this.errors = errors != null ? Collections.unmodifiableSet(errors) : Collections.emptySet();
    }

    /**
     * Creates a valid result.
     * 
     * @return valid validation result
     */
    public static ValidationResult valid() {
        return VALID;
    }

    /**
     * Creates an invalid result with errors.
     * 
     * @param errors validation errors
     * @return invalid validation result
     */
    public static ValidationResult invalid(ValidationError... errors) {
        if (errors == null || errors.length == 0) {
            return valid();
        }
        return new ValidationResult(Set.of(errors));
    }

    /**
     * Creates an invalid result with error set.
     * 
     * @param errors validation errors
     * @return invalid validation result
     */
    public static ValidationResult invalid(Set<ValidationError> errors) {
        if (errors == null || errors.isEmpty()) {
            return valid();
        }
        return new ValidationResult(errors);
    }

    /**
     * Checks if validation was successful.
     * 
     * @return true if valid
     */
    public boolean isValid() {
        return valid;
    }

    /**
     * Gets validation errors.
     * 
     * @return set of validation errors
     */
    public Set<ValidationError> getErrors() {
        return errors;
    }

    /**
     * Combines this result with another result.
     * 
     * @param other the other validation result
     * @return combined result
     */
    public ValidationResult combine(ValidationResult other) {
        if (this.isValid() && other.isValid()) {
            return valid();
        }
        
        Set<ValidationError> combinedErrors = new HashSet<>();
        if (!this.isValid()) {
            combinedErrors.addAll(this.errors);
        }
        if (!other.isValid()) {
            combinedErrors.addAll(other.errors);
        }
        
        return invalid(combinedErrors);
    }

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

    @Override
    public int hashCode() {
        return Objects.hash(valid, errors);
    }

    @Override
    public String toString() {
        return valid ? "ValidationResult{valid}" : "ValidationResult{invalid, errors=" + errors + "}";
    }
}

ValidationError

Represents a validation error.

public class ValidationError {
    private final String message;
    private final String messageKey;
    private final Object[] messageParameters;
    private final String inputHint;

    public ValidationError(String message) {
        this(message, null, null, null);
    }

    public ValidationError(String message, String inputHint) {
        this(message, null, null, inputHint);
    }

    public ValidationError(String messageKey, Object[] messageParameters) {
        this(null, messageKey, messageParameters, null);
    }

    public ValidationError(String messageKey, Object[] messageParameters, String inputHint) {
        this(null, messageKey, messageParameters, inputHint);
    }

    private ValidationError(String message, String messageKey, Object[] messageParameters, String inputHint) {
        this.message = message;
        this.messageKey = messageKey;
        this.messageParameters = messageParameters;
        this.inputHint = inputHint;
    }

    public String getMessage() { return message; }
    public String getMessageKey() { return messageKey; }
    public Object[] getMessageParameters() { return messageParameters; }
    public String getInputHint() { return inputHint; }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        ValidationError that = (ValidationError) obj;
        return Objects.equals(message, that.message) &&
               Objects.equals(messageKey, that.messageKey) &&
               Arrays.equals(messageParameters, that.messageParameters) &&
               Objects.equals(inputHint, that.inputHint);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(message, messageKey, inputHint);
        result = 31 * result + Arrays.hashCode(messageParameters);
        return result;
    }

    @Override
    public String toString() {
        return "ValidationError{" +
               "message='" + message + '\'' +
               ", messageKey='" + messageKey + '\'' +
               ", messageParameters=" + Arrays.toString(messageParameters) +
               ", inputHint='" + inputHint + '\'' +
               '}';
    }
}

Validator Configuration

ValidatorConfig

Configuration for validators.

public class ValidatorConfig {
    private final Map<String, Object> config;

    public ValidatorConfig() {
        this.config = new HashMap<>();
    }

    public ValidatorConfig(Map<String, Object> config) {
        this.config = config != null ? new HashMap<>(config) : new HashMap<>();
    }

    /**
     * Gets a configuration value.
     * 
     * @param key the configuration key
     * @return configuration value or null
     */
    public Object get(String key) {
        return config.get(key);
    }

    /**
     * Gets a configuration value with default.
     * 
     * @param key the configuration key
     * @param defaultValue default value if key not found
     * @return configuration value or default
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String key, T defaultValue) {
        Object value = config.get(key);
        return value != null ? (T) value : defaultValue;
    }

    /**
     * Gets a string configuration value.
     * 
     * @param key the configuration key
     * @return string value or null
     */
    public String getString(String key) {
        Object value = config.get(key);
        return value != null ? value.toString() : null;
    }

    /**
     * Gets a string configuration value with default.
     * 
     * @param key the configuration key
     * @param defaultValue default value
     * @return string value or default
     */
    public String getString(String key, String defaultValue) {
        String value = getString(key);
        return value != null ? value : defaultValue;
    }

    /**
     * Gets an integer configuration value.
     * 
     * @param key the configuration key
     * @param defaultValue default value
     * @return integer value or default
     */
    public int getInt(String key, int defaultValue) {
        Object value = config.get(key);
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value instanceof String) {
            try {
                return Integer.parseInt((String) value);
            } catch (NumberFormatException e) {
                return defaultValue;
            }
        }
        return defaultValue;
    }

    /**
     * Gets a boolean configuration value.
     * 
     * @param key the configuration key
     * @param defaultValue default value
     * @return boolean value or default
     */
    public boolean getBoolean(String key, boolean defaultValue) {
        Object value = config.get(key);
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        if (value instanceof String) {
            return Boolean.parseBoolean((String) value);
        }
        return defaultValue;
    }

    /**
     * Sets a configuration value.
     * 
     * @param key the configuration key
     * @param value the configuration value
     */
    public void put(String key, Object value) {
        if (value == null) {
            config.remove(key);
        } else {
            config.put(key, value);
        }
    }

    /**
     * Gets all configuration as map.
     * 
     * @return configuration map
     */
    public Map<String, Object> asMap() {
        return new HashMap<>(config);
    }
}

Validator Factory

ValidatorFactory

Factory for creating validator instances.

public interface ValidatorFactory extends ProviderFactory<Validator> {
    /**
     * Gets the validator ID.
     * 
     * @return validator ID
     */
    @Override
    String getId();

    /**
     * Creates a validator instance.
     * 
     * @param session Keycloak session
     * @return validator instance
     */
    @Override
    Validator create(KeycloakSession session);
}

ValidatorSPI

SPI for the validation framework.

public class ValidatorSPI implements Spi {
    @Override
    public boolean isInternal() {
        return true;
    }

    @Override
    public String getName() {
        return "validator";
    }

    @Override
    public Class<? extends Provider> getProviderClass() {
        return Validator.class;
    }

    @Override
    public Class<? extends ProviderFactory> getProviderFactoryClass() {
        return ValidatorFactory.class;
    }
}

Abstract Base Classes

AbstractSimpleValidator

Abstract base for simple validators.

public abstract class AbstractSimpleValidator implements SimpleValidator {
    @Override
    public ValidationResult validateValue(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
        if (input == null) {
            return ValidationResult.valid();
        }
        
        return doValidate(input, inputHint, context, config);
    }

    /**
     * Performs the actual validation logic.
     * 
     * @param input the non-null input value
     * @param inputHint hint about the input
     * @param context validation context
     * @param config validator configuration
     * @return validation result
     */
    protected abstract ValidationResult doValidate(Object input, String inputHint, ValidationContext context, ValidatorConfig config);
}

AbstractStringValidator

Abstract base for string validators.

public abstract class AbstractStringValidator extends AbstractSimpleValidator {
    @Override
    protected ValidationResult doValidate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
        String stringValue = input.toString();
        if (stringValue.isEmpty() && ignoreEmptyValues()) {
            return ValidationResult.valid();
        }
        
        return validateString(stringValue, inputHint, context, config);
    }

    /**
     * Validates a string value.
     * 
     * @param value the string value
     * @param inputHint hint about the input
     * @param context validation context
     * @param config validator configuration
     * @return validation result
     */
    protected abstract ValidationResult validateString(String value, String inputHint, ValidationContext context, ValidatorConfig config);

    /**
     * Whether to ignore empty string values.
     * 
     * @return true to ignore empty values
     */
    protected boolean ignoreEmptyValues() {
        return true;
    }
}

Built-in Validators

Validators Utility

Utility class providing access to built-in validators.

public class Validators {
    public static final String LENGTH = "length";
    public static final String EMAIL = "email";
    public static final String PATTERN = "pattern";
    public static final String URI = "uri";
    public static final String INTEGER = "integer";
    public static final String DOUBLE = "double";
    public static final String DATE = "date";
    public static final String PHONE_NUMBER = "phone-number";
    public static final String POSTAL_CODE = "postal-code";

    /**
     * Creates a length validator configuration.
     * 
     * @param min minimum length
     * @param max maximum length
     * @return validator configuration
     */
    public static ValidatorConfig length(int min, int max) {
        ValidatorConfig config = new ValidatorConfig();
        config.put("min", min);
        config.put("max", max);
        return config;
    }

    /**
     * Creates a pattern validator configuration.
     * 
     * @param pattern regular expression pattern
     * @return validator configuration
     */
    public static ValidatorConfig pattern(String pattern) {
        ValidatorConfig config = new ValidatorConfig();
        config.put("pattern", pattern);
        return config;
    }

    /**
     * Creates an email validator configuration.
     * 
     * @return validator configuration
     */
    public static ValidatorConfig email() {
        return new ValidatorConfig();
    }

    /**
     * Creates a URI validator configuration.
     * 
     * @return validator configuration
     */
    public static ValidatorConfig uri() {
        return new ValidatorConfig();
    }

    /**
     * Creates an integer validator configuration.
     * 
     * @param min minimum value
     * @param max maximum value
     * @return validator configuration
     */
    public static ValidatorConfig integer(int min, int max) {
        ValidatorConfig config = new ValidatorConfig();
        config.put("min", min);
        config.put("max", max);
        return config;
    }
}

Usage Examples

Creating Custom Validators

// Custom phone number validator
public class PhoneNumberValidator extends AbstractStringValidator {
    public static final String ID = "phone-number";
    
    private static final Pattern PHONE_PATTERN = Pattern.compile("^\\+?[1-9]\\d{1,14}$");

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

    @Override
    protected ValidationResult validateString(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
        if (!PHONE_PATTERN.matcher(value).matches()) {
            return ValidationResult.invalid(
                new ValidationError("invalid-phone-number", new Object[]{value}, inputHint)
            );
        }
        
        // Additional validation based on country
        String country = config.getString("country");
        if (country != null) {
            return validateForCountry(value, country, inputHint);
        }
        
        return ValidationResult.valid();
    }
    
    private ValidationResult validateForCountry(String phone, String country, String inputHint) {
        // Country-specific validation logic
        switch (country.toUpperCase()) {
            case "US":
                if (!phone.matches("^\\+?1[2-9]\\d{9}$")) {
                    return ValidationResult.invalid(
                        new ValidationError("invalid-us-phone-number", new Object[]{phone}, inputHint)
                    );
                }
                break;
            case "UK":
                if (!phone.matches("^\\+?44[1-9]\\d{8,9}$")) {
                    return ValidationResult.invalid(
                        new ValidationError("invalid-uk-phone-number", new Object[]{phone}, inputHint)
                    );
                }
                break;
        }
        return ValidationResult.valid();
    }
}

// Custom validator factory
public class PhoneNumberValidatorFactory implements ValidatorFactory {
    @Override
    public Validator create(KeycloakSession session) {
        return new PhoneNumberValidator();
    }

    @Override
    public String getId() {
        return PhoneNumberValidator.ID;
    }

    @Override
    public List<ProviderConfigProperty> getConfigMetadata() {
        return Arrays.asList(
            new ProviderConfigProperty("country", "Country Code", "ISO country code for validation", 
                                       ProviderConfigProperty.STRING_TYPE, null)
        );
    }
}

Using Validators in User Profile

// Validate user profile attributes
try (KeycloakSession session = sessionFactory.create()) {
    RealmModel realm = session.realms().getRealmByName("myrealm");
    UserModel user = session.users().getUserByUsername(realm, "john");
    
    // Create validation context
    ValidationContext context = new ValidationContext() {
        @Override
        public KeycloakSession getSession() { return session; }
        
        @Override
        public RealmModel getRealm() { return realm; }
        
        @Override
        public UserModel getUser() { return user; }
        
        @Override
        public Attributes getAttributes() {
            // Return user attributes
            Map<String, List<String>> attrs = user.getAttributes();
            return new AttributesImpl(attrs);
        }
    };
    
    // Validate email
    Validator emailValidator = session.getProvider(Validator.class, Validators.EMAIL);
    ValidatorConfig emailConfig = Validators.email();
    String email = user.getEmail();
    
    ValidationResult emailResult = emailValidator.validate(email, "email", context, emailConfig);
    if (!emailResult.isValid()) {
        System.out.println("Email validation failed: " + emailResult.getErrors());
    }
    
    // Validate phone number with custom validator
    Validator phoneValidator = session.getProvider(Validator.class, PhoneNumberValidator.ID);
    ValidatorConfig phoneConfig = new ValidatorConfig();
    phoneConfig.put("country", "US");
    String phone = user.getFirstAttribute("phoneNumber");
    
    ValidationResult phoneResult = phoneValidator.validate(phone, "phoneNumber", context, phoneConfig);
    if (!phoneResult.isValid()) {
        System.out.println("Phone validation failed: " + phoneResult.getErrors());
    }
    
    // Combine validation results
    ValidationResult combinedResult = emailResult.combine(phoneResult);
    if (!combinedResult.isValid()) {
        System.out.println("Overall validation failed with " + combinedResult.getErrors().size() + " errors");
    }
}

Form Validation

// Validate form input during user registration
public class RegistrationFormValidator {
    private final KeycloakSession session;
    private final RealmModel realm;
    
    public RegistrationFormValidator(KeycloakSession session, RealmModel realm) {
        this.session = session;
        this.realm = realm;
    }
    
    public ValidationResult validateRegistrationForm(MultivaluedMap<String, String> formData) {
        ValidationContext context = createValidationContext();
        
        ValidationResult result = ValidationResult.valid();
        
        // Validate username
        String username = formData.getFirst("username");
        result = result.combine(validateUsername(username, context));
        
        // Validate email
        String email = formData.getFirst("email");
        result = result.combine(validateEmail(email, context));
        
        // Validate password
        String password = formData.getFirst("password");
        result = result.combine(validatePassword(password, context));
        
        // Validate confirm password
        String confirmPassword = formData.getFirst("confirmPassword");
        result = result.combine(validatePasswordConfirmation(password, confirmPassword, context));
        
        // Validate first name
        String firstName = formData.getFirst("firstName");
        result = result.combine(validateFirstName(firstName, context));
        
        // Validate last name
        String lastName = formData.getFirst("lastName");
        result = result.combine(validateLastName(lastName, context));
        
        return result;
    }
    
    private ValidationResult validateUsername(String username, ValidationContext context) {
        if (username == null || username.trim().isEmpty()) {
            return ValidationResult.invalid(new ValidationError("username-required", null, "username"));
        }
        
        // Check length
        Validator lengthValidator = session.getProvider(Validator.class, Validators.LENGTH);
        ValidatorConfig lengthConfig = Validators.length(3, 50);
        ValidationResult lengthResult = lengthValidator.validate(username, "username", context, lengthConfig);
        
        if (!lengthResult.isValid()) {
            return lengthResult;
        }
        
        // Check pattern (alphanumeric and underscore only)
        Validator patternValidator = session.getProvider(Validator.class, Validators.PATTERN);
        ValidatorConfig patternConfig = Validators.pattern("^[a-zA-Z0-9_]+$");
        ValidationResult patternResult = patternValidator.validate(username, "username", context, patternConfig);
        
        if (!patternResult.isValid()) {
            return ValidationResult.invalid(new ValidationError("username-invalid-characters", null, "username"));
        }
        
        // Check uniqueness
        UserModel existingUser = session.users().getUserByUsername(realm, username);
        if (existingUser != null) {
            return ValidationResult.invalid(new ValidationError("username-exists", null, "username"));
        }
        
        return ValidationResult.valid();
    }
    
    private ValidationResult validateEmail(String email, ValidationContext context) {
        if (email == null || email.trim().isEmpty()) {
            return ValidationResult.invalid(new ValidationError("email-required", null, "email"));
        }
        
        Validator emailValidator = session.getProvider(Validator.class, Validators.EMAIL);
        ValidatorConfig emailConfig = Validators.email();
        ValidationResult emailResult = emailValidator.validate(email, "email", context, emailConfig);
        
        if (!emailResult.isValid()) {
            return emailResult;
        }
        
        // Check uniqueness
        UserModel existingUser = session.users().getUserByEmail(realm, email);
        if (existingUser != null) {
            return ValidationResult.invalid(new ValidationError("email-exists", null, "email"));
        }
        
        return ValidationResult.valid();
    }
    
    private ValidationResult validatePassword(String password, ValidationContext context) {
        if (password == null || password.isEmpty()) {
            return ValidationResult.invalid(new ValidationError("password-required", null, "password"));
        }
        
        // Use realm password policy
        PasswordPolicy policy = realm.getPasswordPolicy();
        if (policy != null) {
            try {
                policy.validate(realm.getName(), null, password);
            } catch (PolicyError e) {
                return ValidationResult.invalid(new ValidationError(e.getMessage(), null, "password"));
            }
        }
        
        return ValidationResult.valid();
    }
    
    private ValidationResult validatePasswordConfirmation(String password, String confirmPassword, ValidationContext context) {
        if (confirmPassword == null || confirmPassword.isEmpty()) {
            return ValidationResult.invalid(new ValidationError("password-confirmation-required", null, "confirmPassword"));
        }
        
        if (!Objects.equals(password, confirmPassword)) {
            return ValidationResult.invalid(new ValidationError("passwords-do-not-match", null, "confirmPassword"));
        }
        
        return ValidationResult.valid();
    }
    
    private ValidationResult validateFirstName(String firstName, ValidationContext context) {
        if (firstName == null || firstName.trim().isEmpty()) {
            return ValidationResult.invalid(new ValidationError("first-name-required", null, "firstName"));
        }
        
        Validator lengthValidator = session.getProvider(Validator.class, Validators.LENGTH);
        ValidatorConfig lengthConfig = Validators.length(1, 100);
        return lengthValidator.validate(firstName.trim(), "firstName", context, lengthConfig);
    }
    
    private ValidationResult validateLastName(String lastName, ValidationContext context) {
        if (lastName == null || lastName.trim().isEmpty()) {
            return ValidationResult.invalid(new ValidationError("last-name-required", null, "lastName"));
        }
        
        Validator lengthValidator = session.getProvider(Validator.class, Validators.LENGTH);
        ValidatorConfig lengthConfig = Validators.length(1, 100);
        return lengthValidator.validate(lastName.trim(), "lastName", context, lengthConfig);
    }
    
    private ValidationContext createValidationContext() {
        return new ValidationContext() {
            @Override
            public KeycloakSession getSession() { return session; }
            
            @Override
            public RealmModel getRealm() { return realm; }
            
            @Override
            public UserModel getUser() { return null; }
            
            @Override
            public Attributes getAttributes() { return null; }
        };
    }
}

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