Validation SPI Proposal

Hello Keycloak Developers,

crossposting this from the keycloak-developers Google Group: https://groups.google.com/g/keycloak-dev/c/-XjQu7rn56s

I made some progress with the Validation SPI and I just wanted to give you a quick update on the current state of the API.
If you have questions about the validation SPI or some additional ideas, I’m looking forward to hearing from you.

Validation SPI

Rationale

Currently, validation logic for users, clients and other types is scattered and duplicated all around the Keycloak codebase with partially significant differences concerning the implementation although the validation logic is quite often the same. Also reusing built-in validation logic for custom extensions usually means a lot of duplicated code, which needs to be maintained for Keycloak upgrades.

This proposal introduces a new extensible validation SPI with a uniform facility to register validation rules and to perform validations of arbitrary values while being able to select which validations should be executed in which context. This eases to reuse of existing validation logic and allows the augmentation of the built-in validations with custom validation logic.

This is, of course, no replacement for small static utility functions, e.g. to check whether a value is not null or empty e.g. but rather to describe more complex validation rules that need to be augmented for some use-cases.

Why not just using bean-validation

Well I thought about this for some time but after a quick prototype, I concluded that it would make more sense to go with a custom implementation that leverages the existing Keycloak infrastructure for providing custom extensions and better integration with the existing Keycloak structures. Note that it is still possible to provide a validation/validator implementation that leverages bean validation. Also, bean-validation still works quite well for declarative validation rules in JAX-RS endpoints.

Some features of the Validation SPI

  • Support for Validation of arbitrary Keycloak components on object and property level / single value level
  • Functional interface based Validation function definitions
  • Built-in validations for common values like usernames, email addresses, etc.
  • Support for user-defined custom validations via ValidationProvider SPI
  • Validation Problems can be reported as Errors or Warnings
  • Validations can be performed in bulk mode (all problems are collected for a value) or in short-circuit mode (validation exits on the first problem).
  • Support for nested validations, custom Validations can use other Validations within a validation function
  • Support for validation ordering: validations can be added before or after existing Validations to augment validations, e.g. validate an email for correct format and then check if the email domain is allowed.
  • Support for replacing built-in validations to completely replace a validation if necessary.

Core Abstractions (in server-spi Module)

  • Validation (Functional Interface, describes a Validation)
  • ValidatorProvider (Interface, SPI, denotes a Validator that can validate a given value according to some Validations governed by the given ValidationKey in a ValidationContext)
  • ValidationKey (references a set of validations, like User.USERNAME, User.EMAIL, User.MOBILE_PHONE, etc.)
  • ValidationProvider (Interface, SPI, provides built-in a custom Validations to the validation infrastructure)
  • ValidationContext (A context a validation is performed in, provides access to the current realm, client, session and validation attributes, etc.)
  • ValidationResult (Contains the validation state (valid or not) and access to problems found during the validation run)
  • ValidationProblem (Denotes a concrete problem found during validation (Errors, Warnings) for a ValidationKey an i18n errorMessage reference)
  • ValidationContextKey (Refers to a particular validation context like USER_PROFILE_UPDATE or USER_REGISTRATION, this allows different validations for a validation key in different contexts)
  • ValidationRegistry (Allows to register new Validations for a ValidationKey which can be bound to one or more ValidationContextKeys)

Default implementations (in keycloak-services Module)

  • DefaultValidatorProvider (can discover provided Validations and use them to validate values in a validation context)
  • DefaultValidationRegistry (Allows to register new Validations
  • DefaultValidationProvider (Registers a set of default validation primitives, like validations for email, username, firstname, lastname etc.)

API examples

Registering a new Validation via SPI

public class DefaultValidationProvider implements ValidationProvider {

  @Override
  public void register(MutableValidationRegistry registry) {
...
    registry.register(createEmailValidation(), ValidationKey.User.EMAIL,
        ValidationContextKey.User.PROFILE_UPDATE, ValidationContextKey.User.REGISTRATION);
...
  }
...

  protected Validation createEmailValidation() {
    return (key, value, context) -> {
 
     String input = value instanceof String ? (String) value : null;
 
     if (org.keycloak.services.validation.Validation.isBlank(input)) {
        context.addError(key, Messages.MISSING_EMAIL);
        return false;
      }

      if (!org.keycloak.services.validation.Validation.isEmailValid(input)) {
        context.addError(key, Messages.INVALID_EMAIL);
        return false;
      }
      return true;
    };
  }
}

Using the Validator to Validate a value

ValidatorProvider validator = context.getSession().getProvider(ValidatorProvider.class);

ValidationContext context = new ValidationContext(realm, ValidationContextKey.User.PROFILE_UPDATE, Collections.singletonMap("userNameRequired", userNameRequired));

List<FormMessage> errors = new ArrayList<>();

validator.validate(context, formData.getFirst(FIELD_EMAIL), ValidationKey.User.EMAIL)
       // this can be stream-lined in line with the upcoming user-profile changes
       .accept(res -> res.getErrors().forEach(p -> addError(errors, FIELD_EMAIL, p.getMessage())));
...

PR for current Validation SPI proposal: https://github.com/keycloak/keycloak/pull/7324

Additional Validator examples

// validation with KeycloakSession access

  protected Validation uniqueEmailValidation() {
    return (key, value, context) -> {
      if (context.getRealm().isDuplicateEmailsAllowed()) {
        return true;
      }
      String input = value instanceof String ? (String) value : null;
      if (input == null) {
        context.addError(key, Messages.MISSING_EMAIL);
      }
      UserModel userByEmail = context.getSession().users().getUserByEmail(input, context.getRealm());
      return userByEmail == null;
    };
  }

// validation with nested validation of a complex object (UserModel), e.g. using the built-in email validation

  protected Validation createProfileValidation() {
    return (key, value, context) -> {

      UserModel input = value instanceof UserModel ? (UserModel) value : null;
      if (input == null) {
        context.addError(key, Messages.INVALID_USER);
        return false;
      }

      if (!"content".equals(input.getFirstAttribute("FAKE_FIELD"))) {
        context.addError(key, "FAKE_FIELD_ERRORKEY");
        return false;
      }

      boolean emailValid = context.validateNested(ValidationKey.User.EMAIL, input.getEmail());
      if (!emailValid) {
        context.addError(key, Messages.INVALID_EMAIL);
      }
      return emailValid;
    };
  }

Some more API examples can be found in the DefaultValidatorProviderTest

Additionally, some integration examples can be seen here:

User Profile Proposal integration

Markus Till, Martin Idel, and me had a zoom call this week where we discussed the proposed API and how it could serve as a foundation for the user-profile SPI proposal.
We concluded that a great portion of the current implementation of the user-profile proposal can be implemented on top of the generic validation spi.

We’re currently working on putting the two worlds together.

User-Profile SPI PR: https://github.com/keycloak/keycloak/pull/7155
User-Profile SPI Proposal: https://github.com/keycloak/keycloak-community/blob/master/design/user-profile.md

Cheers,
Thomas

2 Likes