From e2f042bafdb9d2b0bb72761671fc88fb60f8bf0d Mon Sep 17 00:00:00 2001 From: Enoque Duarte Date: Sat, 11 Jul 2026 00:15:32 +0100 Subject: [PATCH 1/2] Add Password Policy SPI chapter to the Server Developer Guide The guide covers every other commonly extended SPI but had no material on password policies. The new chapter documents the provider and factory interfaces, configuration parsing, error message keys, admin console help text, and provider registration. Closes #50803 Signed-off-by: Enoque Duarte --- .../server_development/topics.adoc | 1 + .../topics/password-policy-spi.adoc | 173 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 docs/documentation/server_development/topics/password-policy-spi.adoc diff --git a/docs/documentation/server_development/topics.adoc b/docs/documentation/server_development/topics.adoc index 772127a24a13..e61bacbdda70 100644 --- a/docs/documentation/server_development/topics.adoc +++ b/docs/documentation/server_development/topics.adoc @@ -7,6 +7,7 @@ include::topics/providers.adoc[] ifeval::[{project_community}==true] include::topics/extensions.adoc[] include::topics/auth-spi.adoc[] +include::topics/password-policy-spi.adoc[] include::topics/action-token-handler-spi.adoc[] include::topics/events.adoc[] include::topics/saml-role-mappings-spi.adoc[] diff --git a/docs/documentation/server_development/topics/password-policy-spi.adoc b/docs/documentation/server_development/topics/password-policy-spi.adoc new file mode 100644 index 000000000000..c09c74716ef8 --- /dev/null +++ b/docs/documentation/server_development/topics/password-policy-spi.adoc @@ -0,0 +1,173 @@ +[[_password_policy_spi]] +== Password Policy SPI + +{project_name} ships with a set of built-in password policies, such as minimum length or required special characters, that administrators combine per realm. The link:{adminguide_link}#_password-policies[{adminguide_name}] describes how to configure the built-in policies. When the built-in policies do not cover a requirement, you can implement your own policy with the Password Policy SPI. + +A password policy provider validates a candidate password when a user updates credentials, when an administrator sets a password through the Admin Console or the Admin REST API, and when a user registers. A realm stores its password policy as one string of tokens, for example `length(12) and specialChars(2)`. Each token holds the configuration value for one policy, and your provider receives that value as a single string. + +=== Implementing a password policy provider + +You need to implement the `PasswordPolicyProvider` and `PasswordPolicyProviderFactory` interfaces from the `org.keycloak.policy` package. + +The provider performs the validation. Returning `null` accepts the password, while returning a `PolicyError` rejects it: + +[source,java] +---- +package org.acme.policy; + +import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.policy.PasswordPolicyConfigException; +import org.keycloak.policy.PasswordPolicyProvider; +import org.keycloak.policy.PolicyError; + +public class MaxAcmePasswordPolicyProvider implements PasswordPolicyProvider { + + private final KeycloakContext context; + + public MaxAcmePasswordPolicyProvider(KeycloakContext context) { + this.context = context; + } + + @Override + public PolicyError validate(RealmModel realm, UserModel user, String password) { + return validate(user.getUsername(), password); + } + + @Override + public PolicyError validate(String user, String password) { + int maxOccurrences = context.getRealm().getPasswordPolicy() + .getPolicyConfig(MaxAcmePasswordPolicyProviderFactory.ID); + // return null when the password is acceptable, or a PolicyError with a message key + if (countOccurrences(password, "acme") > maxOccurrences) { + return new PolicyError("invalidPasswordMaxAcmeMessage", maxOccurrences); + } + return null; + } + + private int countOccurrences(String password, String search) { + return (password.length() - password.replace(search, "").length()) / search.length(); + } + + @Override + public Object parseConfig(String value) { + // convert the raw configuration string, and reject invalid values + Integer maxOccurrences = parseInteger(value, 1); + if (maxOccurrences < 0) { + throw new PasswordPolicyConfigException("Number must be zero or greater"); + } + return maxOccurrences; + } + + @Override + public void close() { + } +} +---- + +The value a provider returns from `parseConfig` is cached in the parsed realm password policy and is available through `PasswordPolicy#getPolicyConfig(String providerId)`. Throw a `PasswordPolicyConfigException` from `parseConfig` to reject an invalid configuration value; the Admin Console shows the error to the administrator when the policy is saved. + +The factory describes the policy to the server and to the Admin Console: + +[source,java] +---- +package org.acme.policy; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.policy.PasswordPolicyProvider; +import org.keycloak.policy.PasswordPolicyProviderFactory; + +public class MaxAcmePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory { + + public static final String ID = "maxAcme"; + + @Override + public String getId() { + return ID; + } + + @Override + public PasswordPolicyProvider create(KeycloakSession session) { + return new MaxAcmePasswordPolicyProvider(session.getContext()); + } + + @Override + public String getDisplayName() { + return "Maximum Acme Occurrences"; + } + + @Override + public String getConfigType() { + return PasswordPolicyProvider.INT_CONFIG_TYPE; + } + + @Override + public String getDefaultConfigValue() { + return "1"; + } + + @Override + public boolean isMultiplSupported() { + return false; + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } +} +---- + +The factory methods control how the policy appears in the *Authentication* -> *Policies* tab of the Admin Console: + +* `getDisplayName()` returns the label shown in the *Add policy* drop-down. +* `getConfigType()` declares how the configuration value is edited. The SPI defines two configuration types: `PasswordPolicyProvider.INT_CONFIG_TYPE`, rendered as a numeric input, and `PasswordPolicyProvider.STRING_CONFIG_TYPE`, rendered as a text input. Return `null` when the policy has no editable configuration value; the Admin Console then shows a disabled on/off indicator instead of an input field. +* `getDefaultConfigValue()` returns the value that pre-fills the input when an administrator adds the policy. +* `isMultiplSupported()` reports whether the policy supports multiple instances. The Admin Console currently offers each policy at most once and the realm stores one configuration value per policy, so return `false`. + +=== Policies with multiple configuration parameters + +The SPI does not define structured configuration: the realm password policy holds one configuration string per policy. A policy that needs several parameters therefore has to define its own format inside that single value. One approach is a comma separated list, which the Admin Console accepts inside the parentheses of a policy token: a policy configured as `myPolicy(3,true)` receives the string `3,true` in `parseConfig`. Parse and validate the individual parameters there, apply defaults for the optional trailing ones, and return an object that holds the parsed values. + +=== Returning localized error messages + +The message key passed to `PolicyError` is resolved against the message bundles of the theme that reports the rejection. Login flows, such as registration and required password updates, use the login theme, while the Admin Console and the Admin REST API resolve password errors from the admin theme. Bundle the messages with the provider itself by placing a `theme-resources/messages/messages_en.properties` file in the provider JAR. Files under `theme-resources/messages` are merged into the message bundles of every theme, so one file covers all of these flows: + +[source] +---- +invalidPasswordMaxAcmeMessage=Invalid password: must not contain more than {0} occurrences of "acme". +---- + +See https://www.keycloak.org/ui-customization/themes[Working with themes: Theme resource provider] for more details on theme resources. Add a `messages_.properties` file per supported locale. + +=== Providing help text for the Admin Console + +The Admin Console shows a tooltip for each policy row, resolved from the `admin-messages` bundle using the key `passwordPoliciesHelp.`. Ship the key from the provider JAR in a `theme-resources/messages/admin-messages_en.properties` file: + +[source] +---- +passwordPoliciesHelp.maxAcme=Maximum number of times the string "acme" may occur in the password. +---- + +Without this key the Admin Console shows the raw message key as the tooltip. Add an `admin-messages_.properties` file per supported locale. + +=== Registering the provider + +Register the factory with a `META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory` file in the provider JAR that lists the fully qualified name of the factory class: + +[source] +---- +org.acme.policy.MaxAcmePasswordPolicyProviderFactory +---- + +Deploy the JAR like any other provider, as described in <<_providers,Service Provider Interfaces (SPI)>>. After the server restarts, the policy appears in the *Add policy* drop-down of the *Authentication* -> *Policies* tab. From 4a03a0d6731a8135a6f26534dfb32a69cb95e29c Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Fri, 25 Sep 2026 10:57:07 +0200 Subject: [PATCH 2/2] Review Signed-off-by: Alexander Schwartz --- .../server_development/topics.adoc | 30 +++++++++++++++++++ .../topics/action-token-handler-spi.adoc | 6 ++-- .../server_development/topics/auth-spi.adoc | 1 + .../server_development/topics/events.adoc | 2 ++ .../server_development/topics/extensions.adoc | 3 +- .../topics/partial-private-spi.adoc | 1 + .../topics/password-policy-spi.adoc | 2 ++ 7 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 docs/documentation/server_development/topics/partial-private-spi.adoc diff --git a/docs/documentation/server_development/topics.adoc b/docs/documentation/server_development/topics.adoc index e61bacbdda70..664058b200e9 100644 --- a/docs/documentation/server_development/topics.adoc +++ b/docs/documentation/server_development/topics.adoc @@ -1,31 +1,61 @@ include::topics/preface.adoc[] + include::topics/admin-rest-api.adoc[] + include::topics/identity-brokering.adoc[] + include::topics/identity-brokering/tokens.adoc[] + include::topics/identity-brokering/account-linking.adoc[] + include::topics/providers.adoc[] + ifeval::[{project_community}==true] + include::topics/extensions.adoc[] + include::topics/auth-spi.adoc[] + include::topics/password-policy-spi.adoc[] + include::topics/action-token-handler-spi.adoc[] + include::topics/events.adoc[] + include::topics/saml-role-mappings-spi.adoc[] + endif::[] + include::topics/user-storage.adoc[] + include::topics/user-storage/entity-manager.adoc[] + include::topics/user-storage/provider-interfaces.adoc[] + include::topics/user-storage/provider-capability-interfaces.adoc[] + include::topics/user-storage/model-interfaces.adoc[] + include::topics/user-storage/packaging.adoc[] + include::topics/user-storage/simple-example.adoc[] + include::topics/user-storage/configuration.adoc[] + include::topics/user-storage/registration-query.adoc[] + include::topics/user-storage/augmenting.adoc[] + include::topics/user-storage/import.adoc[] + include::topics/user-storage/cache.adoc[] + include::topics/user-storage/javaee.adoc[] + include::topics/user-storage/rest.adoc[] + include::topics/user-storage/migration.adoc[] + include::topics/user-storage/stream-interfaces.adoc[] + include::topics/vault.adoc[] diff --git a/docs/documentation/server_development/topics/action-token-handler-spi.adoc b/docs/documentation/server_development/topics/action-token-handler-spi.adoc index 9e4806292db7..b1e702720ab5 100644 --- a/docs/documentation/server_development/topics/action-token-handler-spi.adoc +++ b/docs/documentation/server_development/topics/action-token-handler-spi.adoc @@ -1,7 +1,9 @@ [[_action_token_handler_spi]] == Action Token Handler SPI -An action token is a special instance of Json Web Token (JWT) that permits its bearer to perform some actions, e.g. to +include::partial-private-spi.adoc[] + +An action token is a special instance of JSON Web Token (JWT) that permits its bearer to perform some actions, e.g. to reset a password or validate e-mail address. They are usually sent to users in form of a link that points to an endpoint processing action tokens for a particular realm. @@ -136,7 +138,7 @@ public class DemoActionToken extends DefaultActionToken { To plug your own action token and its handler, you need to implement few interfaces on server side: * `org.keycloak.authentication.actiontoken.ActionTokenHandler` - actual handler of action token for a particular - action (i.e. for a given value of `typ` token field). + action (i.e. for a given value of `typ` token field). + The central method in that interface is `handleToken(token, context)` which defines actual operation executed upon receiving the action token. Usually it is some alteration of authentication session notes but generally it can be diff --git a/docs/documentation/server_development/topics/auth-spi.adoc b/docs/documentation/server_development/topics/auth-spi.adoc index 522e86c958ce..8418979ddaac 100644 --- a/docs/documentation/server_development/topics/auth-spi.adoc +++ b/docs/documentation/server_development/topics/auth-spi.adoc @@ -1,6 +1,7 @@ [[_auth_spi]] == Authentication SPI +include::partial-private-spi.adoc[] {project_name} includes a range of different authentication mechanisms: kerberos, password, otp and others. These mechanisms may not meet all of your requirements and you may want to plug in your own custom ones. diff --git a/docs/documentation/server_development/topics/events.adoc b/docs/documentation/server_development/topics/events.adoc index 0a5ad26fa55f..e769c4dff293 100644 --- a/docs/documentation/server_development/topics/events.adoc +++ b/docs/documentation/server_development/topics/events.adoc @@ -1,6 +1,8 @@ [[_events]] == Event Listener SPI +include::partial-private-spi.adoc[] + Writing an Event Listener Provider starts by implementing the `EventListenerProvider` and `EventListenerProviderFactory` interfaces. Please see the Javadoc and examples for complete details on how to do this. diff --git a/docs/documentation/server_development/topics/extensions.adoc b/docs/documentation/server_development/topics/extensions.adoc index 2cbdbed565b6..147383c930aa 100644 --- a/docs/documentation/server_development/topics/extensions.adoc +++ b/docs/documentation/server_development/topics/extensions.adoc @@ -1,5 +1,4 @@ [[_extensions]] - == Extending the server The {project_name} SPI framework offers the possibility to implement or override particular built-in providers. However {project_name} @@ -34,7 +33,7 @@ For details on how to package and deploy a custom provider, refer to the <<_prov NOTE: While it is possible to install other JAX-RS components via the providers extension mechanism, such as filters and interceptors, these are not officially supported. NOTE: To align with Keycloak's built-in transaction per request lifecycle, you should use only blocking, synchronous endpoints. Examples of asynchronous endpoints include -the usage of `AsyncResponse`, or returning `CompletableFuture` or `CompletionStage`. +the usage of `AsyncResponse`, or returning `CompletableFuture` or `CompletionStage`. [[_extensions_spi]] === Add your own custom SPI diff --git a/docs/documentation/server_development/topics/partial-private-spi.adoc b/docs/documentation/server_development/topics/partial-private-spi.adoc new file mode 100644 index 000000000000..fbf561b8948a --- /dev/null +++ b/docs/documentation/server_development/topics/partial-private-spi.adoc @@ -0,0 +1 @@ +NOTE: This is an internal SPI. While it was stable in the previous releases, any new release of {project_name} can change it without prior notice. diff --git a/docs/documentation/server_development/topics/password-policy-spi.adoc b/docs/documentation/server_development/topics/password-policy-spi.adoc index c09c74716ef8..c3c68cd76ef9 100644 --- a/docs/documentation/server_development/topics/password-policy-spi.adoc +++ b/docs/documentation/server_development/topics/password-policy-spi.adoc @@ -1,6 +1,8 @@ [[_password_policy_spi]] == Password Policy SPI +include::partial-private-spi.adoc[] + {project_name} ships with a set of built-in password policies, such as minimum length or required special characters, that administrators combine per realm. The link:{adminguide_link}#_password-policies[{adminguide_name}] describes how to configure the built-in policies. When the built-in policies do not cover a requirement, you can implement your own policy with the Password Policy SPI. A password policy provider validates a candidate password when a user updates credentials, when an administrator sets a password through the Admin Console or the Admin REST API, and when a user registers. A realm stores its password policy as one string of tokens, for example `length(12) and specialChars(2)`. Each token holds the configuration value for one policy, and your provider receives that value as a single string.