Preface
In some of the example listings, what is meant to be displayed on one line does not fit inside the available page width. These lines have been broken up. A '\' at the end of a line means that a break has been introduced to fit in the page, with the following lines indented. So:
Let's pretend to have an extremely \
long line that \
does not fit
This one is short
Is really:
Let's pretend to have an extremely long line that does not fit
This one is short
Admin REST API
Keycloak comes with a fully functional Admin REST API with all features provided by the Admin Console.
To invoke the API you need to obtain an access token with the appropriate permissions. The required permissions are described in the Server Administration Guide.
You can obtain a token by enabling authentication for your application using Keycloak; see the Securing Applications and Services Guide. You can also use direct access grant to obtain an access token.
Examples of using CURL
Authenticating with a username and password
The following example assumes that you created the user admin with the password password in the master realm as shown in the Getting Started Guide tutorial.
|
-
Obtain an access token for the user in the realm
master
with usernameadmin
and passwordpassword
:curl \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=password" \ -d "grant_type=password" \ "http://localhost:8080/realms/master/protocol/openid-connect/token"
By default this token expires in 1 minute The result will be a JSON document.
-
Invoke the API you need by extracting the value of the
access_token
property. -
Invoke the API by including the value in the
Authorization
header of requests to the API.The following example shows how to get the details of the master realm:
curl \ -H "Authorization: bearer eyJhbGciOiJSUz..." \ "http://localhost:8080/admin/realms/master"
Authenticating with a service account
To authenticate against the Admin REST API using a client_id
and a client_secret
, perform this procedure.
-
Make sure the client is configured as follows:
-
client_id
is a confidential client that belongs to the realm master -
client_id
hasService Accounts Enabled
option enabled -
client_id
has a custom "Audience" mapper-
Included Client Audience:
security-admin-console
-
-
-
Check that
client_id
has the role 'admin' assigned in the "Service Account Roles" tab.
curl \
-d "client_id=<YOUR_CLIENT_ID>" \
-d "client_secret=<YOUR_CLIENT_SECRET>" \
-d "grant_type=client_credentials" \
"http://localhost:8080/realms/master/protocol/openid-connect/token"
Themes
Keycloak provides theme support for web pages and emails. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
Theme types
A theme can provide one or more types to customize different aspects of Keycloak. The types available are:
-
Account - Account Console
-
Admin - Admin Console
-
Email - Emails
-
Login - Login forms
-
Welcome - Welcome page
Configuring a theme
All theme types, except welcome, are configured through the Admin Console.
-
Log into the Admin Console.
-
Select your realm from the drop-down box in the top left corner.
-
Click Realm Settings from the menu.
-
Click the Themes tab.
To set the theme for the master
Admin Console you need to set the Admin Console theme for themaster
realm. -
To see the changes to the Admin Console refresh the page.
-
Change the welcome theme by using the
spi-theme-welcome-theme
option. -
For example:
bin/kc.[sh|bat] start --spi-theme-welcome-theme=custom-theme
Default themes
Keycloak comes bundled with default themes in the JAR file keycloak-themes-26.0.2.jar
inside the server distribution.
The server’s root themes
directory does not contain any themes by default, but it contains a README file with some additional details about the default themes.
To simplify upgrading, do not edit the bundled themes directly. Instead create your own theme that extends one of the bundled themes.
Creating a theme
A theme consists of:
-
HTML templates (Freemarker Templates)
-
Images
-
Message bundles
-
Stylesheets
-
Scripts
-
Theme properties
Unless you plan to replace every single page you should extend another theme. Most likely you will want to extend some existing theme. Alternatively, if you intend to provide your own implementation of the admin or account console,
consider extending the base
theme. The base
theme consists of a message bundle and therefore such implementation needs to start from scratch, including implementation of the main index.ftl
Freemarker template, but it can leverage existing translations from the message bundle.
When extending a theme you can override individual resources (templates, stylesheets, etc.). If you decide to override HTML templates bear in mind that you may need to update your custom template when upgrading to a new release.
While creating a theme it’s a good idea to disable caching as this makes it possible to edit theme resources directly from the themes
directory without
restarting Keycloak.
-
Run Keycloak with the following options:
bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false
-
Create a directory in the
themes
directory.The name of the directory becomes the name of the theme. For example to create a theme called
mytheme
create the directorythemes/mytheme
. -
Inside the theme directory, create a directory for each of the types your theme is going to provide.
For example, to add the login type to the
mytheme
theme, create the directorythemes/mytheme/login
. -
For each type create a file
theme.properties
which allows setting some configuration for the theme.For example, to configure the theme
themes/mytheme/login
to extend thebase
theme and import some common resources, create the filethemes/mytheme/login/theme.properties
with following contents:parent=base import=common/keycloak
You have now created a theme with support for the login type.
-
Log into the Admin Console to check out your new theme
-
Select your realm
-
Click Realm Settings from the menu.
-
Click on the Themes tab.
-
For Login Theme select mytheme and click Save.
-
Open the login page for the realm.
You can do this either by logging in through your application or by opening the Account Console (
/realms/{realm name}/account
). -
To see the effect of changing the parent theme, set
parent=keycloak
intheme.properties
and refresh the login page.
Be sure to re-enable caching in production as it will significantly impact performance. |
If you want to manually delete the content of the themes cache, you can do so by deleting the |
Theme properties
Theme properties are set in the file <THEME TYPE>/theme.properties
in the theme directory.
-
parent - Parent theme to extend
-
import - Import resources from another theme
-
common - Override the common resource path. The default value is
common/keycloak
when not specified. This value would be used as value of suffix of${url.resourcesCommonPath}
, which is used typically in freemarker templates (prefix of${url.resoucesCommonPath}
value is theme root uri). -
styles - Space-separated list of styles to include
-
locales - Comma-separated list of supported locales
There are a list of properties that can be used to change the css class used for certain element types. For a list of these properties look at the theme.properties
file in the corresponding type of the keycloak theme (themes/keycloak/<THEME TYPE>/theme.properties
).
You can also add your own custom properties and use them from custom templates.
When doing so, you can substitute system properties or environment variables by using these formats:
-
${some.system.property}
- for system properties -
${env.ENV_VAR}
- for environment variables.
A default value can also be provided in case the system property or the environment variable is not found with ${foo:defaultValue}
.
If no default value is provided and there’s no corresponding system property or environment variable, then nothing is replaced and you end up with the format in your template. |
Here’s an example of what is possible:
javaVersion=${java.version}
unixHome=${env.HOME:Unix home not found}
windowsHome=${env.HOMEPATH:Windows home not found}
Add a stylesheet to a theme
You can add one or more stylesheets to a theme.
-
Create a file in the
<THEME TYPE>/resources/css
directory of your theme. -
Add this file to the
styles
property intheme.properties
.For example, to add
styles.css
to themytheme
, createthemes/mytheme/login/resources/css/styles.css
with the following content:.login-pf body { background: DimGrey none; }
-
Edit
themes/mytheme/login/theme.properties
and add:styles=css/styles.css
-
To see the changes, open the login page for your realm.
You will notice that the only styles being applied are those from your custom stylesheet.
-
To include the styles from the parent theme, load the styles from that theme. Edit
themes/mytheme/login/theme.properties
and changestyles
to:styles=css/login.css css/styles.css
To override styles from the parent stylesheets, ensure that your stylesheet is listed last.
Adding a script to a theme
You can add one or more scripts to a theme.
-
Create a file in the
<THEME TYPE>/resources/js
directory of your theme. -
Add the file to the
scripts
property intheme.properties
.For example, to add
script.js
to themytheme
, createthemes/mytheme/login/resources/js/script.js
with the following content:alert('Hello');
Then edit
themes/mytheme/login/theme.properties
and add:scripts=js/script.js
Adding an image to a theme
To make images available to the theme add them to the <THEME TYPE>/resources/img
directory of your theme. These can be used from within stylesheets or
directly in HTML templates.
For example to add an image to the mytheme
copy an image to themes/mytheme/login/resources/img/image.jpg
.
You can then use this image from within a custom stylesheet with:
body {
background-image: url('../img/image.jpg');
background-size: cover;
}
Or to use directly in HTML templates add the following to a custom HTML template:
<img src="${url.resourcesPath}/img/image.jpg" alt="My image description">
Adding a custom footer to a login theme
In order to use a custom footer, create a footer.ftl
file in your custom login theme with the desired content.
An example for a custom footer.ftl
may look like this:
<#macro content>
<#-- footer at the end of the login box -->
<div>
<ul id="kc-login-footer-links">
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<div>
</#macro>
Adding an image to an email theme
To make images available to the theme add them to the <THEME TYPE>/email/resources/img
directory of your theme. These can be used from within directly in HTML templates.
For example to add an image to the mytheme
copy an image to themes/mytheme/email/resources/img/logo.jpg
.
To use directly in HTML templates add the following to a custom HTML template:
<img src="${url.resourcesUrl}/img/image.jpg" alt="My image description">
Messages
Text in the templates is loaded from message bundles. A theme that extends another theme will inherit all messages from the parent’s message bundle and you can
override individual messages by adding <THEME TYPE>/messages/messages_en.properties
to your theme.
For example to replace Username
on the login form with Your Username
for the mytheme
create the file
themes/mytheme/login/messages/messages_en.properties
with the following content:
usernameOrEmail=Your Username
Within a message values like {0}
and {1}
are replaced with arguments when the message is used. For example {0} in Log in to {0}
is replaced with the name
of the realm.
Texts of these message bundles can be overwritten by realm-specific values. The realm-specific values are manageable via UI and API.
Adding a language to a realm
-
To enable internationalization for a realm, see the Server Administration Guide.
-
Create the file
<THEME TYPE>/messages/messages_<LOCALE>.properties
in the directory of your theme. -
Add this file to the
locales
property in<THEME TYPE>/theme.properties
. For a language to be available to users the realmslogin
,account
andemail
, the theme has to support the language, so you need to add your language for those theme types.For example, to add Norwegian translations to the
mytheme
theme create the filethemes/mytheme/login/messages/messages_no.properties
with the following content:usernameOrEmail=Brukernavn password=Passord
If you omit a translation for messages, they will use English.
-
Edit
themes/mytheme/login/theme.properties
and add:locales=en,no
-
Add the same for the
account
andemail
theme types. To do this createthemes/mytheme/account/messages/messages_no.properties
andthemes/mytheme/email/messages/messages_no.properties
. Leaving these files empty will result in the English messages being used. -
Copy
themes/mytheme/login/theme.properties
tothemes/mytheme/account/theme.properties
andthemes/mytheme/email/theme.properties
. -
Add a translation for the language selector. This is done by adding a message to the English translation. To do this add the following to
themes/mytheme/account/messages/messages_en.properties
andthemes/mytheme/login/messages/messages_en.properties
:locale_no=Norsk
By default, message properties files should be encoded using UTF-8.
Keycloak falls back to ISO-8859-1 handling if it can’t read the contents as UTF-8.
Unicode characters can be escaped as described in Java’s documentation for PropertyResourceBundle.
Previous versions of Keycloak supported specifying the encoding in the first line with a comment like # encoding: UTF-8
, which is no longer supported.
-
See Locale Selector for details on how the current locale is selected.
Adding custom Identity Providers icons
Keycloak supports adding icons for custom Identity providers, which are displayed on the login screen.
-
Define icon classes in your login
theme.properties
file (for example,themes/mytheme/login/theme.properties
) with key patternkcLogoIdP-<alias>
. -
For an Identity Provider with an alias
myProvider
, you may add a line totheme.properties
file of your custom theme. For example:kcLogoIdP-myProvider = fa fa-lock
All icons are available on the official website of PatternFly4.
Icons for social providers are already defined in base
login theme properties (themes/keycloak/login/theme.properties
), where you can inspire yourself.
Creating a custom HTML template
Keycloak uses Apache Freemarker templates to generate HTML and render pages.
Although it is possible to create custom templates to change completely how pages are rendered, the recommendation is to leverage the built-in templates as much as possible. The reasons are:
-
During upgrades, you might be forced to update your custom templates to get the latest updates from newer versions
-
Configuring CSS styles to your themes allows you to adapt the UI to match your UI design standards and guidelines.
-
User Profile allows you to support custom user attributes and configure how they are rendered.
In most cases, you won’t need to change templates to adapt Keycloak to your needs, but you can override individual
templates in your own theme by creating <THEME TYPE>/<TEMPLATE>.ftl
.
Admin and account console use a single template index.ftl
for rendering the application.
For a list of templates in other theme types look at the theme/base/<THEME_TYPE>
directory in the JAR file at $KEYCLOAK_HOME/lib/lib/main/org.keycloak.keycloak-themes-<VERSION>.jar
.
-
Copy the template from the base theme to your own theme.
-
Apply the modifications you need.
For example, to create a custom login form for the
mytheme
theme, copythemes/base/login/login.ftl
tothemes/mytheme/login
and open it in an editor.After the first line (<#import …>), add
<h1>HELLO WORLD!</h1>
as shown here:<#import "template.ftl" as layout> <h1>HELLO WORLD!</h1> ...
-
Back up the modified template. When upgrading to a new version of Keycloak you may need to update your custom templates to apply changes to the original template if applicable.
-
See the FreeMarker Manual for details on how to edit templates.
Emails
To edit the subject and contents for emails, for example password recovery email, add a message bundle to the email
type of your theme. There are three messages for each email. One for the subject, one for the plain text body and one for the html body.
To see all emails available take a look at themes/base/email/messages/messages_en.properties
.
For example to change the password recovery email for the mytheme
theme create themes/mytheme/email/messages/messages_en.properties
with the following
content:
passwordResetSubject=My password recovery
passwordResetBody=Reset password link: {0}
passwordResetBodyHtml=<a href="{0}">Reset password</a>
Deploying themes
Themes can be deployed to Keycloak by copying the theme directory to themes
or it can be deployed as an archive. During development you can copy the
theme to the themes
directory, but in production you may want to consider using an archive
. An archive
makes it simpler to have a versioned copy of
the theme, especially when you have multiple instances of Keycloak for example with clustering.
-
To deploy a theme as an archive, create a JAR archive with the theme resources.
-
Add a file
META-INF/keycloak-themes.json
to the archive that lists the available themes in the archive as well as what types each theme provides.For example for the
mytheme
theme createmytheme.jar
with the contents:-
META-INF/keycloak-themes.json
-
theme/mytheme/login/theme.properties
-
theme/mytheme/login/login.ftl
-
theme/mytheme/login/resources/css/styles.css
-
theme/mytheme/login/resources/img/image.png
-
theme/mytheme/login/messages/messages_en.properties
-
theme/mytheme/email/messages/messages_en.properties
The contents of
META-INF/keycloak-themes.json
in this case would be:{ "themes": [{ "name" : "mytheme", "types": [ "login", "email" ] }] }
A single archive can contain multiple themes and each theme can support one or more types.
-
To deploy the archive to Keycloak, add it to the providers/
directory of
Keycloak and restart the server if it is already running.
Additional resources for Themes
-
For the inspiration, see Default Themes bundled inside Keycloak.
-
Keycloak Quickstarts Repository - Directory
extension
of the quickstarts repository contains some theme examples, which can be also used as an inspiration.
Themes based on React
The admin console and account console are based on React. To fully customize these you can use the React based npm packages. There are two packages:
-
@keycloak/keycloak-admin-ui
: This is the base theme for the admin console. -
@keycloak/keycloak-account-ui
: This is the base theme for the account console.
Both packages are available on npm.
Installing the packages
To install the packages, run the following command:
pnpm install @keycloak/keycloak-account-ui
Using the packages
To use these pages you’ll need to add KeycloakProvider in your component hierarchy to setup what client, realm and url to use.
import { KeycloakProvider } from "@keycloak/keycloak-ui-shared";
//...
<KeycloakProvider environment={{
serverBaseUrl: "http://localhost:8080",
realm: "master",
clientId: "security-admin-console"
}}>
{/* rest of you application */}
</KeycloakProvider>
Translating the pages
The pages are translated using the i18next
library.
You can set it up as described on their [website](https://react.i18next.com/).
If you want to use the translations that are provided then you need to add i18next-http-backend to your project and add:
backend: {
loadPath: `http://localhost:8080/resources/master/account/{lng}}`,
parse: (data: string) => {
const messages = JSON.parse(data);
const result: Record<string, string> = {};
messages.forEach((v) => (result[v.key] = v.value)); //need to convert to record
return result;
},
},
Using the pages
All "pages" are React components that can be used in your application. To see what components are available, see the [source](https://github.com/keycloak/keycloak/blob/main/js/apps/account-ui/src/index.ts). Or have a look at the [quick start](https://github.com/keycloak/keycloak-quickstarts/tree/main/extension/extend-admin-console-node) to see how to use them.
Theme selector
By default the theme configured for the realm is used, with the exception of clients being able to override the login theme. This behavior can be changed through the Theme Selector SPI.
This could be used to select different themes for desktop and mobile devices by looking at the user agent header, for example.
To create a custom theme selector you need to implement ThemeSelectorProviderFactory
and ThemeSelectorProvider
.
Theme resources
When implementing custom providers in Keycloak there may often be a need to add additional templates, resources and messages bundles.
An example use-case would be a custom authenticator that requires additional templates and resources.
The easiest way to load additional theme resources is to create a JAR with templates in theme-resources/templates
resources in theme-resources/resources
and messages bundles in theme-resources/messages
.
If you want a more flexible way to load templates and resources that can be achieved through the ThemeResourceSPI.
By implementing ThemeResourceProviderFactory
and ThemeResourceProvider
you can decide exactly how to load templates
and resources.
Locale selector
By default, the locale is selected using the DefaultLocaleSelectorProvider
which implements the LocaleSelectorProvider
interface. English is the default language when internationalization is disabled.
With internationalization enabled, the locale is resolved according to the logic described in the Server Administration Guide.
This behavior can be changed through the LocaleSelectorSPI
by implementing the LocaleSelectorProvider
and LocaleSelectorProviderFactory
.
The LocaleSelectorProvider
interface has a single method, resolveLocale
, which must return a locale given a RealmModel
and a nullable UserModel
. The actual request is available from the KeycloakSession#getContext
method.
Custom implementations can extend the DefaultLocaleSelectorProvider
in order to reuse parts of the default behavior. For example to ignore the Accept-Language
request header, a custom implementation could extend the default provider, override it’s getAcceptLanguageHeaderLocale
, and return a null value. As a result the locale selection will fall back on the realm’s default language.
Additional resources for Locale selector
-
For more details on creating and deploying a custom provider, see Service Provider Interfaces.
Identity Brokering APIs
Keycloak can delegate authentication to a parent IDP for login. A typical example of this is the case where you want users to be able to log in through a social provider such as Facebook or Google. You can also link existing accounts to a brokered IDP. This section describes some APIs that your applications can use as it pertains to identity brokering.
Retrieving external IDP tokens
Keycloak allows you to store tokens and responses from the authentication process with the external IDP.
For that, you can use the Store Token
configuration option on the IDP’s settings page.
Application code can retrieve these tokens and responses to pull in extra user information, or to securely invoke requests on the external IDP. For example, an application might want to use the Google token to invoke on other Google services and REST APIs. To retrieve a token for a particular identity provider you need to send a request as follows:
GET /realms/{realm}/broker/{provider_alias}/token HTTP/1.1
Host: localhost:8080
Authorization: Bearer <KEYCLOAK ACCESS TOKEN>
An application must have authenticated with Keycloak and have received an access token. This access token
will need to have the broker
client-level role read-token
set. This means that the user must have a role mapping for this role
and the client application must have that role within its scope.
In this case, given that you are accessing a protected service in Keycloak, you need to send the access token issued by Keycloak during the user authentication.
In the broker configuration page you can automatically assign this role to newly imported users by turning on the Stored Tokens Readable
switch.
These external tokens can be re-established by either logging in again through the provider, or using the client initiated account linking API.
Client initiated account linking
Some applications want to integrate with social providers like Facebook, but do not want to provide an option to login via these social providers. Keycloak offers a browser-based API that applications can use to link an existing user account to a specific external IDP. This is called client-initiated account linking. Account linking can only be initiated by OIDC applications.
The way it works is that the application forwards the user’s browser to a URL on the Keycloak server requesting that it wants to link the user’s account to a specific external provider (i.e. Facebook). The server initiates a login with the external provider. The browser logs in at the external provider and is redirected back to the server. The server establishes the link and redirects back to the application with a confirmation.
There are some preconditions that must be met by the client application before it can initiate this protocol:
-
The desired identity provider must be configured and enabled for the user’s realm in the admin console.
-
The user account must already be logged in as an existing user via the OIDC protocol
-
The user must have an
account.manage-account
oraccount.manage-account-links
role mapping. -
The application must be granted the scope for those roles within its access token
-
The application must have access to its access token as it needs information within it to generate the redirect URL.
To initiate the login, the application must fabricate a URL and redirect the user’s browser to this URL. The URL looks like this:
/{auth-server-root}/realms/{realm}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}
Here’s a description of each path and query param:
- provider
-
This is the provider alias of the external IDP that you defined in the
Identity Provider
section of the admin console. - client_id
-
This is the OIDC client id of your application. When you registered the application as a client in the admin console, you had to specify this client id.
- redirect_uri
-
This is the application callback URL you want to redirect to after the account link is established. It must be a valid client redirect URI pattern. In other words, it must match one of the valid URL patterns you defined when you registered the client in the admin console.
- nonce
-
This is a random string that your application must generate
- hash
-
This is a Base64 URL encoded hash. This hash is generated by Base64 URL encoding a SHA_256 hash of
nonce
+token.getSessionState()
+token.getIssuedFor()
+provider
. The token variable are obtained from the OIDC access token. Basically you are hashing the random nonce, the user session id, the client id, and the identity provider alias you want to access.
Here’s an example of Java Servlet code that generates the URL to establish the account link.
KeycloakSecurityContext session = (KeycloakSecurityContext) httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
AccessToken token = session.getToken();
String clientId = token.getIssuedFor();
String nonce = UUID.randomUUID().toString();
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
String input = nonce + token.getSessionState() + clientId + provider;
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
String hash = Base64Url.encode(check);
request.getSession().setAttribute("hash", hash);
String redirectUri = ...;
String accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl)
.path("/realms/{realm}/broker/{provider}/link")
.queryParam("nonce", nonce)
.queryParam("hash", hash)
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri).build(realm, provider).toString();
Why is this hash included? We do this so that the auth server is guaranteed to know that the client application initiated the request and no other rogue app just randomly asked for a user account to be linked to a specific provider. The auth server will first check to see if the user is logged in by checking the SSO cookie set at login. It will then try to regenerate the hash based on the current login and match it up to the hash sent by the application.
After the account has been linked, the auth server will redirect back to the redirect_uri
. If there is a problem servicing the link request,
the auth server may or may not redirect back to the redirect_uri
. The browser may just end up at an error page instead of being redirected back
to the application. If there is an error condition and the auth server deems it safe enough to redirect back to the client app, an additional
error
query parameter will be appended to the redirect_uri
.
While this API guarantees that the application initiated the request, it does not completely prevent CSRF attacks for this operation. The application is still responsible for guarding against CSRF attacks target at itself. |
Service Provider Interfaces (SPI)
Keycloak is designed to cover most use-cases without requiring custom code, but we also want it to be customizable. To achieve this Keycloak has a number of Service Provider Interfaces (SPI) for which you can implement your own providers.
Implementing an SPI
To implement an SPI you need to implement its ProviderFactory and Provider interfaces. You also need to create a service configuration file.
For example, to implement the Theme Selector SPI you need to implement ThemeSelectorProviderFactory and ThemeSelectorProvider and also provide the file
META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory
.
Example ThemeSelectorProviderFactory:
package org.acme.provider;
import ...
public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory {
@Override
public ThemeSelectorProvider create(KeycloakSession session) {
return new MyThemeSelectorProvider(session);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "myThemeSelector";
}
}
It is recommended that your provider factory implementation returns unique id by method getId()
. However
there can be some exceptions to this rule as mentioned below in the Overriding providers section.
Keycloak creates a single instance of provider factories which makes it possible to store state for multiple requests. Provider instances are created by calling create on the factory for each request so these should be light-weight object. |
Example ThemeSelectorProvider:
package org.acme.provider;
import ...
public class MyThemeSelectorProvider implements ThemeSelectorProvider {
public MyThemeSelectorProvider(KeycloakSession session) {
}
@Override
public String getThemeName(Theme.Type type) {
return "my-theme";
}
@Override
public void close() {
}
}
Example service configuration file (META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory
):
org.acme.provider.MyThemeSelectorProviderFactory
To configure your provider, see the Configuring Providers guide.
For example, to configure a provider you can set options as follows:
bin/kc.[sh|bat] --spi-theme-selector-my-theme-selector-enabled=true --spi-theme-selector-my-theme-selector-theme=my-theme
Then you can retrieve the config in the ProviderFactory
init method:
public void init(Config.Scope config) {
String themeName = config.get("theme");
}
Your provider can also look up other providers if needed. For example:
public class MyThemeSelectorProvider implements ThemeSelectorProvider {
private KeycloakSession session;
public MyThemeSelectorProvider(KeycloakSession session) {
this.session = session;
}
@Override
public String getThemeName(Theme.Type type) {
return session.getContext().getRealm().getLoginTheme();
}
}
Override built-in providers
As mentioned above, it is recommended that your ProviderFactory
implementations use unique ID. However at the same time, it can be useful to override one of the Keycloak built-in providers.
The recommended way for this is still ProviderFactory implementation with unique ID and then for instance set the default provider as
specified in the Configuring Providers guide. On the other hand, this may not be always possible.
For instance when you need some customizations to default OpenID Connect protocol behaviour and you want to override
default Keycloak implementation of OIDCLoginProtocolFactory
you need to preserve same providerId. As for example admin console, OIDC protocol well-known endpoint and various other things rely on
the ID of the protocol factory being openid-connect
.
For this case, it is highly recommended to implement method order()
of your custom implementation and make sure that it has higher order than the built-in implementation.
public class CustomOIDCLoginProtocolFactory extends OIDCLoginProtocolFactory {
// Some customizations here
@Override
public int order() {
return 1;
}
}
In case of multiple implementations with same provider ID, only the one with highest order will be used by Keycloak runtime.
Show info from your SPI implementation in the Admin Console
Sometimes it is useful to show additional info about your Provider to a Keycloak administrator. You can show provider build time information (for example, version of custom provider currently installed), current configuration of the provider (e.g. url of remote system your provider talks to) or some operational info (average time of response from remote system your provider talks to). Keycloak Admin Console provides Server Info page to show this kind of information.
To show info from your provider it is enough to implement org.keycloak.provider.ServerInfoAwareProviderFactory
interface in your ProviderFactory
.
Example implementation for MyThemeSelectorProviderFactory
from previous example:
package org.acme.provider;
import ...
public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory, ServerInfoAwareProviderFactory {
...
@Override
public Map<String, String> getOperationalInfo() {
Map<String, String> ret = new LinkedHashMap<>();
ret.put("theme-name", "my-theme");
return ret;
}
}
Use available providers
In your provider implementation, you can use other providers available in Keycloak. The existing providers can be typically retrieved with the
usage of the KeycloakSession
, which is available to your provider as described in the section Implementing an SPI.
Keycloak has two provider types:
-
Single-implementation provider types - There can be only a single active implementation of the particular provider type in Keycloak runtime.
For example
HostnameProvider
specifies the hostname to be used by Keycloak and that is shared for the whole Keycloak server. Hence there can be only single implementation of this provider active for the Keycloak server. If there are multiple provider implementations available to the server runtime, one of them needs to be specified as the default one.
For example such as:
bin/kc.[sh|bat] build --spi-hostname-provider=default
The value default
used as the value of default-provider
must match the ID returned by the ProviderFactory.getId()
of the particular provider factory implementation.
In the code, you can obtain the provider such as keycloakSession.getProvider(HostnameProvider.class)
-
Multiple implementation provider types - Those are provider types, that allow multiple implementations available and working together in the Keycloak runtime.
For example
EventListener
provider allows to have multiple implementations available and registered, which means that particular event can be sent to all the listeners (jboss-logging, sysout etc). In the code, you can obtain a specified instance of the provider for example such assession.getProvider(EventListener.class, "jboss-logging")
. You need to specifyprovider_id
of the provider as the second argument as there can be multiple instances of this provider type as described above.The provider ID must match the ID returned by the
ProviderFactory.getId()
of the particular provider factory implementation. Some provider types can be retrieved with the usage ofComponentModel
as the second argument and some (for exampleAuthenticator
) even need to be retrieved with the usage ofKeycloakSessionFactory
. It is not recommended to implement your own providers this way as it may be deprecated in the future.
Registering provider implementations
Providers are registered with the server by simply copying the JAR file to the providers
directory.
If your provider needs additional dependencies not already provided by Keycloak copy these to the providers
directory.
After registering new providers or dependencies Keycloak needs to be re-built with a non-optimized start or the kc.[sh|bat] build
command.
Provider JARs are not loaded in isolated classloaders, so do not include resources or classes in your provider JARs that conflict with built-in resources or classes. In particular the inclusion of an application.properties file or overriding the commons-lang3 dependency will cause auto-build to fail if the provider JAR is removed. If you have included conflicting classes, you may see a split package warning in the start log for the server. Unfortunately not all built-in lib jars are checked by the split package warning logic, so you’ll need to check the lib directory JARs before bundling or including a transitive dependency. Should there be a conflict, that can be resolved by removing or repackaging the offending classes. There is no warning if you have conflicting resource files. You should either ensure that your JAR’s resource files have path names that contain something unique to that provider,
or you can check for the existence of
If you find that your server will not start due to a
This will force Quarkus to rebuild the classloading related index files. From there you should be able to perform a non-optimized start or build without an exception. |
JavaScript providers
Scripts is Preview and is not fully supported. This feature is disabled by default. To enable start the server with |
Keycloak has the ability to execute scripts during runtime in order to allow administrators to customize specific functionalities:
-
Authenticator
-
JavaScript Policy
-
OpenID Connect Protocol Mapper
-
SAML Protocol Mapper
Authenticator
Authentication scripts must provide at least one of the following functions:
authenticate(..)
, which is called from Authenticator#authenticate(AuthenticationFlowContext)
action(..)
, which is called from Authenticator#action(AuthenticationFlowContext)
Custom Authenticator
should at least provide the authenticate(..)
function.
You can use the javax.script.Bindings
script within the code.
script
-
the
ScriptModel
to access script metadata realm
-
the
RealmModel
user
-
the current
UserModel
. Note thatuser
is available when your script authenticator is configured in the authentication flow in a way that is triggered after another authenticator succeeded in establishing user identity and set the user into the authentication session. session
-
the active
KeycloakSession
authenticationSession
-
the current
AuthenticationSessionModel
httpRequest
-
the current
org.jboss.resteasy.spi.HttpRequest
LOG
-
a
org.jboss.logging.Logger
scoped toScriptBasedAuthenticator
You can extract additional context information from the context argument passed to the authenticate(context) action(context) function.
|
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
function authenticate(context) {
LOG.info(script.name + " --> trace auth for: " + user.username);
if ( user.username === "tester"
&& user.getAttribute("someAttribute")
&& user.getAttribute("someAttribute").contains("someValue")) {
context.failure(AuthenticationFlowError.INVALID_USER);
return;
}
context.success();
}
Where to add script authenticator
A possible use of script authenticator is to do some checks at the end of the authentication. Note that if you want your script authenticator to be always triggered (even for instance during SSO re-authentication with the identity cookie), you may need to add it as REQUIRED at the end of the authentication flow and encapsulate the existing authenticators into a separate REQUIRED authentication subflow. This need is because the REQUIRED and ALTERNATIVE executions should not be at the same level. For example, the authentication flow configuration should appear as follows:
- User-authentication-subflow REQUIRED
-- Cookie ALTERNATIVE
-- Identity-provider-redirect ALTERNATIVE
...
- Your-Script-Authenticator REQUIRED
OpenID Connect Protocol Mapper
OpenID Connect Protocol Mapper scripts are javascript script that allow you to change the content of the ID Token and/or the Access Token.
You can use the javax.script.Bindings
script within the code.
user
-
the current
UserModel
realm
-
the
RealmModel
token
-
the current
IDToken
. It is available only if the mapper is configured for the ID token. tokenResponse
-
the current
AccessTokenResponse
. It is available only if the mapper is configured for the Access token. userSession
-
the active
UserSessionModel
keycloakSession
-
the active
KeycloakSession
The exports of the script will be used as the value of the token claim.
// prints can be used to log information for debug purpose.
print("STARTING CUSTOM MAPPER");
var inputRequest = keycloakSession.getContext().getHttpRequest();
var params = inputRequest.getDecodedFormParameters();
var output = params.getFirst("user_input");
exports = output;
The above script allows to retrieve a user_input
from the authorization request.
This will be available to map in the Token Claim Name
configured in the mapper.
Create a JAR with the scripts to deploy
JAR files are regular ZIP files with a .jar extension.
|
In order to make your scripts available to Keycloak you need to deploy them to the server. For that, you should create
a JAR
file with the following structure:
META-INF/keycloak-scripts.json
my-script-authenticator.js
my-script-policy.js
my-script-mapper.js
The META-INF/keycloak-scripts.json
is a file descriptor that provides metadata information about the scripts you want to deploy. It is a JSON file with the following structure:
{
"authenticators": [
{
"name": "My Authenticator",
"fileName": "my-script-authenticator.js",
"description": "My Authenticator from a JS file"
}
],
"policies": [
{
"name": "My Policy",
"fileName": "my-script-policy.js",
"description": "My Policy from a JS file"
}
],
"mappers": [
{
"name": "My Mapper",
"fileName": "my-script-mapper.js",
"description": "My Mapper from a JS file"
}
],
"saml-mappers": [
{
"name": "My Mapper",
"fileName": "my-script-mapper.js",
"description": "My Mapper from a JS file"
}
]
}
This file should reference the different types of script providers that you want to deploy:
-
authenticators
For OpenID Connect Script Authenticators. You can have one or multiple authenticators in the same JAR file
-
policies
For JavaScript Policies when using Keycloak Authorization Services. You can have one or multiple policies in the same JAR file
-
mappers
For OpenID Connect Script Protocol Mappers. You can have one or multiple mappers in the same JAR file
-
saml-mappers
For SAML Script Protocol Mappers. You can have one or multiple mappers in the same JAR file
For each script file in your JAR
file, you need a corresponding entry in META-INF/keycloak-scripts.json
that maps your scripts files to a specific provider type. For that you should provide the following properties for each entry:
-
name
A friendly name that will be used to show the scripts through the Keycloak Administration Console. If not provided, the name of the script file will be used instead
-
description
An optional text that better describes the intend of the script file
-
fileName
The name of the script file. This property is mandatory and should map to a file within the JAR.
Available SPIs
If you want to see list of all available SPIs at runtime, you can check Server Info
page in Admin Console as described in Admin Console section.
ExampleSpi
Extending the server
The Keycloak SPI framework offers the possibility to implement or override particular built-in providers. However Keycloak also provides capabilities to extend its core functionalities and domain. This includes possibilities to:
-
Add custom REST endpoints to the Keycloak server
-
Add your own custom SPI
-
Add custom JPA entities to the Keycloak data model
Add custom REST endpoints
This is a very powerful extension, which allows you to deploy your own REST endpoints to the Keycloak server. It enables all kinds of extensions, for example the possibility to trigger functionality on the Keycloak server, which is not available through the default set of built-in Keycloak REST endpoints.
To add a custom REST endpoint, you need to implement the RealmResourceProviderFactory
and RealmResourceProvider
interfaces. RealmResourceProvider
has one important method:
Object getResource();
Use this method to return an object, which acts as a JAX-RS Resource.
Your JAX-RS resource is only recognized by the server and registered as a valid endpoint if it includes the following configuration:
- adding an empty file named beans.xml
under META-INF
- annotating the JAX-RS class with the annotation jakarta.ws.rs.ext.Provider
.
For details on how to package and deploy a custom provider, refer to the Service Provider Interfaces chapter.
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. |
Add your own custom SPI
A custom SPI is especially useful with Custom REST endpoints. Use this procedure to add your own SPI
-
implement the interface
org.keycloak.provider.Spi
and define the ID of your SPI and theProviderFactory
andProvider
classes. That looks like this:public class ExampleSpi implements Spi { @Override public boolean isInternal() { return false; } @Override public String getName() { return "example"; } @Override public Class<? extends Provider> getProviderClass() { return ExampleService.class; } @Override @SuppressWarnings("rawtypes") public Class<? extends ProviderFactory> getProviderFactoryClass() { return ExampleServiceProviderFactory.class; } }
-
Create the file
META-INF/services/org.keycloak.provider.Spi
and add the class of your SPI to it. For example:ExampleSpi
-
Create the interfaces
ExampleServiceProviderFactory
, which extends fromProviderFactory
andExampleService
, which extends fromProvider
. TheExampleService
will usually contain the business methods you need for your use case. Note that theExampleServiceProviderFactory
instance is always scoped per application, howeverExampleService
is scoped per-request (or more accurately perKeycloakSession
lifecycle). -
Finally you need to implement your providers in the same manner as described in the Service Provider Interfaces chapter.
Add custom JPA entities to the Keycloak data model
If the Keycloak data model does not exactly match your desired solution, or if you want to add some core functionality to Keycloak,
or when you have your own REST endpoint, you might want to extend the Keycloak data model. We enable you to add your
own JPA entities to the Keycloak JPA EntityManager
.
To add your own JPA entities, you need to implement JpaEntityProviderFactory
and JpaEntityProvider
. The JpaEntityProvider
allows you to return a list of your custom JPA entities and provide the location and id of the Liquibase changelog. An example implementation can look like this:
This is an unsupported API, which means you can use it but there is no guarantee that it will not be removed or changed without warning. |
public class ExampleJpaEntityProvider implements JpaEntityProvider {
// List of your JPA entities.
@Override
public List<Class<?>> getEntities() {
return Collections.<Class<?>>singletonList(Company.class);
}
// This is used to return the location of the Liquibase changelog file.
// You can return null if you don't want Liquibase to create and update the DB schema.
@Override
public String getChangelogLocation() {
return "META-INF/example-changelog.xml";
}
// Helper method, which will be used internally by Liquibase.
@Override
public String getFactoryId() {
return "sample";
}
...
}
In the example above, we added a single JPA entity represented by class Company
. In the code of your REST endpoint, you can then use something like
this to retrieve EntityManager
and call DB operations on it.
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
Company myCompany = em.find(Company.class, "123");
The methods getChangelogLocation
and getFactoryId
are important to support automatic updating of your entities by Liquibase. Liquibase
is a framework for updating the database schema, which Keycloak internally uses to create the DB schema and update the DB schema among versions. You may need to use
it as well and create a changelog for your entities. Note that versioning of your own Liquibase changelog is independent
of Keycloak versions. In other words, when you update to a new Keycloak version, you are not forced to update your
schema at the same time. And vice versa, you can update your schema even without updating the Keycloak version. The Liquibase update
is always done at the server startup, so to trigger a DB update of your schema, you just need to add the new changeset to your Liquibase changelog file (in the example above
it’s the file META-INF/example-changelog.xml
which must be packed in same JAR as the JPA entities and ExampleJpaEntityProvider
) and then restart server.
The DB schema will be automatically updated at startup.
Don’t forget to always back up your database before doing any changes in the Liquibase changelog and triggering a DB update. |
Authentication SPI
Keycloak 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. Keycloak provides an authentication SPI that you can use to write new plugins. The Admin Console supports applying, ordering, and configuring these new mechanisms.
Keycloak also supports a simple registration form. Different aspects of this form can be enabled and disabled for example reCAPTCHA support can be turned off and on. The same authentication SPI can be used to add another page to the registration flow or reimplement it entirely. There’s also an additional fine-grained SPI you can use to add specific validations and user extensions to the built-in registration form.
A required action in Keycloak is an action that a user has to perform after he authenticates. After the action is performed successfully, the user doesn’t have to perform the action again. Keycloak comes with some built in required actions like "reset password". This action forces the user to change their password after they have logged in. You can write and plug in your own required actions.
If your authenticator or required action implementation is using some user attributes as the metadata attributes for linking/establishing the user identity, then please make sure that users are not able to edit the attributes and the corresponding attributes are read-only. See the details in the Threat model mitigation chapter. |
Terms
To first learn about the Authentication SPI, let’s go over some of the terms used to describe it.
- Authentication Flow
-
A flow is a container for all authentications that must happen during login or registration. If you go to the Admin Console authentication page, you can view all the defined flows in the system and what authenticators they are made up of. Flows can contain other flows. You can also bind a new different flow for browser login, direct grant access, and registration.
- Authenticator
-
An authenticator is a pluggable component that hold the logic for performing the authentication or action within a flow. It is usually a singleton.
- Execution
-
An execution is an object that binds the authenticator to the flow and the authenticator to the configuration of the authenticator. Flows contain execution entries.
- Execution Requirement
-
Each execution defines how an authenticator behaves in a flow. The requirement defines whether the authenticator is enabled, disabled, conditional, required, or an alternative. An alternative requirement means that the authenticator is enough to validate the flow it’s in, but isn’t necessary. For example, in the built-in browser flow, cookie authentication, the Identity Provider Redirector, and the set of all authenticators in the forms subflow are all alternative. As they are executed in a sequential top-to-bottom order, if one of them is successful, the flow is successful, and any following execution in the flow (or sub-flow) is not evaluated.
- Authenticator Config
-
This object defines the configuration for the Authenticator for a specific execution within an authentication flow. Each execution can have a different config.
- Required Action
-
After authentication completes, the user might have one or more one-time actions he must complete before he is allowed to login. The user might be required to set up an OTP token generator or reset an expired password or even accept a Terms and Conditions document.
Algorithm overview
Let’s talk about how this all works for browser login. Let’s assume the following flows, executions and sub flows.
Cookie - ALTERNATIVE
Kerberos - ALTERNATIVE
Forms subflow - ALTERNATIVE
Username/Password Form - REQUIRED
Conditional OTP subflow - CONDITIONAL
Condition - User Configured - REQUIRED
OTP Form - REQUIRED
In the top level of the form we have 3 executions of which all are alternatively required. This means that if any of these are successful, then the others do not have to execute. The Username/Password form is not executed if there is an SSO Cookie set or a successful Kerberos login. Let’s walk through the steps from when a client first redirects to keycloak to authenticate the user.
-
The OpenID Connect or SAML protocol provider unpacks relevant data, verifies the client and any signatures. It creates an AuthenticationSessionModel. It looks up what the browser flow should be, then starts executing the flow.
-
The flow looks at the cookie execution and sees that it is an alternative. It loads the cookie provider. It checks to see if the cookie provider requires that a user already be associated with the authentication session. Cookie provider does not require a user. If it did, the flow would abort and the user would see an error screen. Cookie provider then executes. Its purpose is to see if there is an SSO cookie set. If there is one set, it is validated and the UserSessionModel is verified and associated with the AuthenticationSessionModel. The Cookie provider returns a success() status if the SSO cookie exists and is validated. Since the cookie provider returned success and each execution at this level of the flow is ALTERNATIVE, no other execution is executed and this results in a successful login. If there is no SSO cookie, the cookie provider returns with a status of attempted(). This means there was no error condition, but no success either. The provider tried, but the request just wasn’t set up to handle this authenticator.
-
Next the flow looks at the Kerberos execution. This is also an alternative. The kerberos provider also does not require a user to be already set up and associated with the AuthenticationSessionModel so this provider is executed. Kerberos uses the SPNEGO browser protocol. This requires a series of challenge/responses between the server and client exchanging negotiation headers. The kerberos provider does not see any negotiate header, so it assumes that this is the first interaction between the server and client. It therefore creates an HTTP challenge response to the client and sets a forceChallenge() status. A forceChallenge() means that this HTTP response cannot be ignored by the flow and must be returned to the client. If instead the provider returned a challenge() status, the flow would hold the challenge response until all other alternatives are attempted. So, in this initial phase, the flow would stop and the challenge response would be sent back to the browser. If the browser then responds with a successful negotiate header, the provider associates the user with the AuthenticationSession and the flow ends because the rest of the executions on this level of the flow are all alternatives. Otherwise, again, the kerberos provider sets an attempted() status and the flow continues.
-
The next execution is a subflow called Forms. The executions for this subflow are loaded and the same processing logic occurs.
-
The first execution in the Forms subflow is the UsernamePassword provider. This provider also does not require for a user to already be associated with the flow. This provider creates a challenge HTTP response and sets its status to challenge(). This execution is required, so the flow honors this challenge and sends the HTTP response back to the browser. This response is a rendering of the Username/Password HTML page. The user enters in their username and password and clicks submit. This HTTP request is directed to the UsernamePassword provider. If the user entered an invalid username or password, a new challenge response is created and a status of failureChallenge() is set for this execution. A failureChallenge() means that there is a challenge, but that the flow should log this as an error in the error log. This error log can be used to lock accounts or IP Addresses that have had too many login failures. If the username and password is valid, the provider associated the UserModel with the AuthenticationSessionModel and returns a status of success().
-
The next execution is a subflow called Conditional OTP. The executions for this subflow are loaded and the same processing logic occurs. Its Requirement is Conditional. This means that the flow will first evaluate all conditional executors that it contains. Conditional executors are authenticators that implement
ConditionalAuthenticator
, and must implement the methodboolean matchCondition(AuthenticationFlowContext context)
. A conditional subflow will call thematchCondition
method of all conditional executions it contains, and if all of them evaluate to true, it will act as if it was a required subflow. If not, it will act as if it was a disabled subflow. Conditional authenticators are only used for this purpose, and are not used as authenticators. This means that even if the conditional authenticator evaluates to "true", then this will not mark a flow or subflow as successful. For example, a flow containing only a Conditional subflow with only a conditional authenticator will never allow a user to log in. -
The first execution of the Conditional OTP subflow is the Condition - User Configured. This provider requires that a user has been associated with the flow. This requirement is satisfied because the UsernamePassword provider already associated the user with the flow. This provider’s
matchCondition
method will evaluate theconfiguredFor
method for all other Authenticators in its current subflow. If the subflow contains executors with their Requirement set to required, then thematchCondition
method will only evaluate to true if all the required authenticators'configuredFor
method evaluate to true. Otherwise, thematchCondition
method will evaluate to true if any alternative authenticator evaluates to true. -
The next execution is the OTP Form. This provider also requires that a user has been associated with the flow. This requirement is satisfied because the UsernamePassword provider already associated the user with the flow. Since a user is required for this provider, the provider is also asked if the user is configured to use this provider. If user is not configured, then the flow will then set up a required action that the user must perform after authentication is complete. For OTP, this means the OTP setup page. If the user is configured, he will be asked to enter his otp code. In our scenario, because of the conditional sub-flow, the user will never see the OTP login page, unless the Conditional OTP subflow is set to Required.
-
After the flow is complete, the authentication processor creates a UserSessionModel and associates it with the AuthenticationSessionModel. It then checks to see if the user is required to complete any required actions before logging in.
-
First, each required action’s evaluateTriggers() method is called. This allows the required action provider to figure out if there is some state that might trigger the action to be fired. For example, if your realm has a password expiration policy, it might be triggered by this method.
-
Each required action associated with the user that has its requiredActionChallenge() method called. Here the provider sets up an HTTP response which renders the page for the required action. This is done by setting a challenge status.
-
If the required action is ultimately successful, then the required action is removed from the user’s required actions list.
-
After all required actions have been resolved, the user is finally logged in.
Authenticator SPI walk through
In this section, we’ll take a look at the Authenticator interface.
For this, we are going to implement an authenticator that requires that a user enter in the answer to a secret question like "What is your mother’s maiden name?".
This example is fully implemented and contained in the Keycloak Quickstarts Repository repository under extension/authenticator
.
To create an authenticator, you must at minimum implement the org.keycloak.authentication.AuthenticatorFactory and Authenticator interfaces. The Authenticator interface defines the logic. The AuthenticatorFactory is responsible for creating instances of an Authenticator. They both extend a more generic Provider and ProviderFactory set of interfaces that other Keycloak components like User Federation do.
Some authenticators, like the CookieAuthenticator don’t rely on a Credential that the user has or knows to authenticate the user. However, some authenticators, such as the PasswordForm authenticator or the OTPFormAuthenticator rely on the user inputting some information and verifying that information against some information in the database. For the PasswordForm for example, the authenticator will verify the hash of the password against a hash stored in the database, while the OTPFormAuthenticator will verify the OTP received against the one generated from the shared secret stored in the database.
These types of authenticators are called CredentialValidators, and will require you to implement a few more classes:
-
A class that extends org.keycloak.credential.CredentialModel, and that can generate the correct format of the credential in the database
-
A class implementing the org.keycloak.credential.CredentialProvider and interface, and a class implementing its CredentialProviderFactory factory interface.
The SecretQuestionAuthenticator we’ll see in this walk through is a CredentialValidator, so we’ll see how to implement all these classes.
Packaging classes and deployment
You will package your classes within a single jar.
This jar must contain a file named org.keycloak.authentication.AuthenticatorFactory
and must be contained in the META-INF/services/
directory of your jar.
This file must list the fully qualified class name of each AuthenticatorFactory implementation you have in the jar.
For example:
org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
org.keycloak.examples.authenticator.AnotherProviderFactory
This services/ file is used by Keycloak to scan the providers it has to load into the system.
To deploy this jar, just copy it to the providers directory.
Extending the CredentialModel class
In Keycloak, credentials are stored in the database in the Credentials table. It has the following structure:
----------------------------- | ID | ----------------------------- | user_ID | ----------------------------- | credential_type | ----------------------------- | created_date | ----------------------------- | user_label | ----------------------------- | secret_data | ----------------------------- | credential_data | ----------------------------- | priority | -----------------------------
Where:
-
ID
is the primary key of the credential. -
user_ID
is the foreign key linking the credential to a user. -
credential_type
is a string set during the creation that must reference an existing credential type. -
created_date
is the creation timestamp (in long format) of the credential. -
user_label
is the editable name of the credential by the user -
secret_data
contains a static json with the information that cannot be transmitted outside of Keycloak -
credential_data
contains a json with the static information of the credential that can be shared in the Admin Console or via the REST API. -
priority
defines how "preferred" a credential is for a user, to determine which credential to present when a user has multiple choices.
As the secret_data and credential_data fields are designed to contain json, it is up to you to determine how to structure, read and write into these fields, allowing you a lot of flexibility.
For this example, we are going to use a very simple credential data, containing only the question asked to the user:
{
"question":"aQuestion"
}
with an equally simple secret data, containing only the secret answer:
{
"answer":"anAnswer"
}
Here the answer will be kept in plain text in the database for the sake of simplicity, but it would also be possible to have a salted hash for the answer,
as is the case for passwords in Keycloak. In this case, the secret data would also have to contain a field for the salt, and the credential data information
about the algorithm such as the type of algorithm used and the number of iterations used. For more details you can consult the implementation of the
org.keycloak.models.credential.PasswordCredentialModel
class.
In our case we create the class SecretQuestionCredentialModel
:
public class SecretQuestionCredentialModel extends CredentialModel {
public static final String TYPE = "SECRET_QUESTION";
private final SecretQuestionCredentialData credentialData;
private final SecretQuestionSecretData secretData;
Where TYPE
is the credential_type we write in the database. For consistency, we make sure that this String is always the one referenced when
getting the type for this credential. The classes SecretQuestionCredentialData
and SecretQuestionSecretData
are used to marshal and unmarshal the json:
public class SecretQuestionCredentialData {
private final String question;
@JsonCreator
public SecretQuestionCredentialData(@JsonProperty("question") String question) {
this.question = question;
}
public String getQuestion() {
return question;
}
}
public class SecretQuestionSecretData {
private final String answer;
@JsonCreator
public SecretQuestionSecretData(@JsonProperty("answer") String answer) {
this.answer = answer;
}
public String getAnswer() {
return answer;
}
}
To be fully usable, the SecretQuestionCredentialModel
objects must both contain the raw json data from its parent class,
and the unmarshalled objects in its own attributes. This leads us to create a method which reads from a simple CredentialModel,
such as is created when reading from the database, to make a SecretQuestionCredentialModel
:
private SecretQuestionCredentialModel(SecretQuestionCredentialData credentialData, SecretQuestionSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
}
public static SecretQuestionCredentialModel createFromCredentialModel(CredentialModel credentialModel){
try {
SecretQuestionCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), SecretQuestionCredentialData.class);
SecretQuestionSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), SecretQuestionSecretData.class);
SecretQuestionCredentialModel secretQuestionCredentialModel = new SecretQuestionCredentialModel(credentialData, secretData);
secretQuestionCredentialModel.setUserLabel(credentialModel.getUserLabel());
secretQuestionCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
secretQuestionCredentialModel.setType(TYPE);
secretQuestionCredentialModel.setId(credentialModel.getId());
secretQuestionCredentialModel.setSecretData(credentialModel.getSecretData());
secretQuestionCredentialModel.setCredentialData(credentialModel.getCredentialData());
return secretQuestionCredentialModel;
} catch (IOException e){
throw new RuntimeException(e);
}
}
And a method to create a SecretQuestionCredentialModel
from the question and answer:
private SecretQuestionCredentialModel(String question, String answer) {
credentialData = new SecretQuestionCredentialData(question);
secretData = new SecretQuestionSecretData(answer);
}
public static SecretQuestionCredentialModel createSecretQuestion(String question, String answer) {
SecretQuestionCredentialModel credentialModel = new SecretQuestionCredentialModel(question, answer);
credentialModel.fillCredentialModelFields();
return credentialModel;
}
private void fillCredentialModelFields(){
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setType(TYPE);
setCreatedDate(Time.currentTimeMillis());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Implementing a CredentialProvider
As with all Providers, to allow Keycloak to generate the CredentialProvider, we require a CredentialProviderFactory. For this requirement we create
the SecretQuestionCredentialProviderFactory, whose create
method will be called when a SecretQuestionCredentialProvider is asked for:
public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory<SecretQuestionCredentialProvider> {
public static final String PROVIDER_ID = "secret-question";
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public CredentialProvider create(KeycloakSession session) {
return new SecretQuestionCredentialProvider(session);
}
}
The CredentialProvider interface takes a generic parameter that extends a CredentialModel. In our case we to use the SecretQuestionCredentialModel we created:
public class SecretQuestionCredentialProvider implements CredentialProvider<SecretQuestionCredentialModel>, CredentialInputValidator {
private static final Logger logger = Logger.getLogger(SecretQuestionCredentialProvider.class);
protected KeycloakSession session;
public SecretQuestionCredentialProvider(KeycloakSession session) {
this.session = session;
}
We also want to implement the CredentialInputValidator interface, as this allows Keycloak to know that this provider can also be used to validate a
credential for an Authenticator. For the CredentialProvider interface, the first method that needs to be implemented is the getType()
method. This will simply
return the `SecretQuestionCredentialModel’s TYPE String:
@Override
public String getType() {
return SecretQuestionCredentialModel.TYPE;
}
The second method is to create a SecretQuestionCredentialModel
from a CredentialModel
. For this method we simply call the existing static method
from SecretQuestionCredentialModel
:
@Override
public SecretQuestionCredentialModel getCredentialFromModel(CredentialModel model) {
return SecretQuestionCredentialModel.createFromCredentialModel(model);
}
Finally, we have the methods to create a credential and delete a credential. These methods call the UserModel’s credential manager, which is responsible for knowing where to read or write the credential, for example local storage or federated storage.
@Override
public CredentialModel createCredential(RealmModel realm, UserModel user, SecretQuestionCredentialModel credentialModel) {
if (credentialModel.getCreatedDate() == null) {
credentialModel.setCreatedDate(Time.currentTimeMillis());
}
return user.credentialManager().createStoredCredential(credentialModel);
}
@Override
public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {
return user.credentialManager().removeStoredCredentialById(credentialId);
}
For the CredentialInputValidator, the main method to implement is the isValid
, which tests whether a credential is valid for a
given user in a given realm. This is the method that is called by the Authenticator when it seeks to validate the user’s input. Here we
simply need to check that the input String is the one recorded in the Credential:
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) {
logger.debug("Expected instance of UserCredentialModel for CredentialInput");
return false;
}
if (!input.getType().equals(getType())) {
return false;
}
String challengeResponse = input.getChallengeResponse();
if (challengeResponse == null) {
return false;
}
CredentialModel credentialModel = getCredentialStore().getStoredCredentialById(realm, user, input.getCredentialId());
SecretQuestionCredentialModel sqcm = getCredentialFromModel(credentialModel);
return sqcm.getSecretQuestionSecretData().getAnswer().equals(challengeResponse);
}
The other two methods to implement are a test if the CredentialProvider supports the given credential type and a test to check if the credential type is configured for a given user. For our case, the latter test simply means checking if the user has a credential of the SECRET_QUESTION type:
@Override
public boolean supportsCredentialType(String credentialType) {
return getType().equals(credentialType);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
if (!supportsCredentialType(credentialType)) return false;
return !getCredentialStore().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
}
Implementing an authenticator
When implementing an authenticator that uses Credentials to authenticate a user, you should have the authenticator implement
the CredentialValidator interface. This interfaces takes a class extending a CredentialProvider as a parameter, and will
allow Keycloak to directly call the methods from the CredentialProvider. The only method that needs to be implemented is
getCredentialProvider
method, which in our example allows the SecretQuestionAuthenticator to retrieve the SecretQuestionCredentialProvider:
public SecretQuestionCredentialProvider getCredentialProvider(KeycloakSession session) {
return (SecretQuestionCredentialProvider)session.getProvider(CredentialProvider.class, SecretQuestionCredentialProviderFactory.PROVIDER_ID);
}
When implementing the Authenticator interface, the first method that needs to be implemented is the requiresUser() method. For our example, this method must return true as we need to validate the secret question associated with the user. A provider like kerberos would return false from this method as it can resolve a user from the negotiate header. This example, however, is validating a specific credential of a specific user.
The next method to implement is the configuredFor() method. This method is responsible for determining if the user is configured for this particular authenticator. In our case, we can just call the method implemented in the SecretQuestionCredentialProvider
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return getCredentialProvider(session).isConfiguredFor(realm, user, getType(session));
}
The next method to implement on the Authenticator is setRequiredActions(). If configuredFor() returns false and our example authenticator
is required within the flow, this method will be called, but only if the associated AuthenticatorFactory’s isUserSetupAllowed
method returns true.
The setRequiredActions() method is responsible for registering any required actions that must be performed by the user.
In our example, we need to register a required action that will force the user to set up the answer to the secret question.
We will implement this required action provider later in this chapter.
Here is the implementation of the setRequiredActions() method.
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
user.addRequiredAction("SECRET_QUESTION_CONFIG");
}
Now we are getting into the meat of the Authenticator implementation. The next method to implement is authenticate(). This is the initial method the flow invokes when the execution is first visited. What we want is that if a user has answered the secret question already on their browser’s machine, then the user doesn’t have to answer the question again, making that machine "trusted". The authenticate() method isn’t responsible for processing the secret question form. Its sole purpose is to render the page or to continue the flow.
@Override
public void authenticate(AuthenticationFlowContext context) {
if (hasCookie(context)) {
context.success();
return;
}
Response challenge = context.form()
.createForm("secret-question.ftl");
context.challenge(challenge);
}
protected boolean hasCookie(AuthenticationFlowContext context) {
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
boolean result = cookie != null;
if (result) {
System.out.println("Bypassing secret question because cookie is set");
}
return result;
}
The hasCookie() method checks to see if there is already a cookie set on the browser which indicates that the secret question has already been answered. If that returns true, we just mark this execution’s status as SUCCESS using the AuthenticationFlowContext.success() method and returning from the authentication() method.
If the hasCookie() method returns false, we must return a response that renders the secret question HTML form.
AuthenticationFlowContext has a form() method that initializes a Freemarker page builder with appropriate base information needed to build the form.
This page builder is called org.keycloak.login.LoginFormsProvider
. The LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme.
Additionally you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the Freemarker template.
We’ll go over this later.
Calling LoginFormsProvider.createForm() returns a JAX-RS Response object. We then call AuthenticationFlowContext.challenge() passing in this response. This sets the status of the execution as CHALLENGE and if the execution is Required, this JAX-RS Response object will be sent to the browser.
So, the HTML page asking for the answer to a secret question is displayed to the user and the user enters in the answer and clicks submit. The action URL of the HTML form will send an HTTP request to the flow. The flow will end up invoking the action() method of our Authenticator implementation.
@Override
public void action(AuthenticationFlowContext context) {
boolean validated = validateAnswer(context);
if (!validated) {
Response challenge = context.form()
.setError("badSecret")
.createForm("secret-question.ftl");
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
return;
}
setCookie(context);
context.success();
}
If the answer is not valid, we rebuild the HTML Form with an additional error message. We then call AuthenticationFlowContext.failureChallenge() passing in the reason for the value and the JAX-RS response. failureChallenge() works the same as challenge(), but it also records the failure so it can be analyzed by any attack detection service.
If validation is successful, then we set a cookie to remember that the secret question has been answered and we call AuthenticationFlowContext.success().
The validation itself gets the data that was received from the form, and calls the isValid method from the SecretQuestionCredentialProvider. You’ll notice
that there’s a section of the code concerning getting the credential Id. This is because if Keycloak is configured to allow multiple types of alternative
authenticators, or if the user could record multiple credentials of the SECRET_QUESTION type (for example if we allowed to choose from several questions,
and we allowed the user to have answers for more than one of those questions), then Keycloak needs to know which credential is being used to log the user.
In case there is more than one credential, Keycloak allows the user to choose during the login which credential is being used, and the information is transmitted by
the form to the Authenticator.
In case the form doesn’t present this information, credential id used is given by the CredentialProvider’s default getDefaultCredential
method, which will
return the "most preferred" credential of the correct type of the user,
protected boolean validateAnswer(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String secret = formData.getFirst("secret_answer");
String credentialId = formData.getFirst("credentialId");
if (credentialId == null || credentialId.isEmpty()) {
credentialId = getCredentialProvider(context.getSession())
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
}
UserCredentialModel input = new UserCredentialModel(credentialId, getType(context.getSession()), secret);
return getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input);
}
Next method is the setCookie(). This is an example of providing configuration for the Authenticator. In this case we want the max age of the cookie to be configurable.
protected void setCookie(AuthenticationFlowContext context) {
AuthenticatorConfigModel config = context.getAuthenticatorConfig();
int maxCookieAge = 60 * 60 * 24 * 30; // 30 days
if (config != null) {
maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
}
URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build();
addCookie(context, "SECRET_QUESTION_ANSWERED", "true",
uri.getRawPath(),
null, null,
maxCookieAge,
false, true);
}
We obtain an AuthenticatorConfigModel from the AuthenticationFlowContext.getAuthenticatorConfig() method. If configuration exists we pull the max age config out of it. We will see how we can define what should be configured when we talk about the AuthenticatorFactory implementation. The config values can be defined within the Admin Console if you set up config definitions in your AuthenticatorFactory implementation.
@Override
public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {
return CredentialTypeMetadata.builder()
.type(getType())
.category(CredentialTypeMetadata.Category.TWO_FACTOR)
.displayName(SecretQuestionCredentialProviderFactory.PROVIDER_ID)
.helpText("secret-question-text")
.createAction(SecretQuestionAuthenticatorFactory.PROVIDER_ID)
.removeable(false)
.build(session);
}
The last method to implement in the SecretQuestionCredentialProvider class is getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext), which is an abstract method of the CredentialProvider interface. Each Credential provider has to provide and implement this method. The method returns an instance of CredentialTypeMetadata, which should at least include type and category of authenticator, displayName and removable item. In this example, the builder takes type of authenticator from method getType(), category is Two Factor (the authenticator can be used as second factor of authentication) and removable, which is set up to false (user can’t remove some previously registered credentials).
Other items of builder are helpText (will be shown to the user on various screens), createAction (the providerID of the required action, which can be used by the user to create new credential) or updateAction (same as createAction, but instead of creating the new credential, it will update the credential).
Implementing an AuthenticatorFactory
The next step in this process is to implement an AuthenticatorFactory. This factory is responsible for instantiating an Authenticator. It also provides deployment and configuration metadata about the Authenticator.
The getId() method is just the unique name of the component. The create() method is called by the runtime to allocate and process the Authenticator.
public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
public static final String PROVIDER_ID = "secret-question-authenticator";
private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
The next thing the factory is responsible for is to specify the allowed requirement switches. While there are four different requirement types: ALTERNATIVE, REQUIRED, CONDITIONAL, DISABLED, AuthenticatorFactory implementations can limit which requirement options are shown in the Admin Console when defining a flow. CONDITIONAL should only always be used for subflows, and unless there’s a good reason for doing otherwise, the requirement on an authenticator should be REQUIRED, ALTERNATIVE and DISABLED:
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.DISABLED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
The AuthenticatorFactory.isUserSetupAllowed() is a flag that tells the flow manager whether or not Authenticator.setRequiredActions() method will be called. If an Authenticator is not configured for a user, the flow manager checks isUserSetupAllowed(). If it is false, then the flow aborts with an error. If it returns true, then the flow manager will invoke Authenticator.setRequiredActions().
@Override
public boolean isUserSetupAllowed() {
return true;
}
The next few methods define how the Authenticator can be configured. The isConfigurable() method is a flag which specifies to the Admin Console on whether the Authenticator can be configured within a flow. The getConfigProperties() method returns a list of ProviderConfigProperty objects. These objects define a specific configuration attribute.
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName("cookie.max.age");
property.setLabel("Cookie Max Age");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
configProperties.add(property);
}
Each ProviderConfigProperty defines the name of the config property. This is the key used in the config map stored in AuthenticatorConfigModel. The label defines how the config option will be displayed in the Admin Console. The type defines if it is a String, Boolean, or other type. The Admin Console will display different UI inputs depending on the type. The help text is what will be shown in the tooltip for the config attribute in the Admin Console. Read the javadoc of ProviderConfigProperty for more detail.
The rest of the methods are for the Admin Console. getHelpText() is the tooltip text that will be shown when you are picking the Authenticator you want to bind to an execution. getDisplayType() is the text that will be shown in the Admin Console when listing the Authenticator. getReferenceCategory() is just a category the Authenticator belongs to.
Adding an authenticator form
Keycloak comes with a Freemarker theme and template engine.
The createForm() method you called within authenticate() of your Authenticator class, builds an HTML page from a file within your login theme: secret-question.ftl
.
This file should be added to the theme-resources/templates
in your JAR, see Theme Resource Provider for more details.
Let’s take a bigger look at secret-question.ftl Here’s a small code snippet:
<form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="totp" class="${properties.kcLabelClass!}">${msg("loginSecretQuestion")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
</div>
</div>
</form>
Any piece of text enclosed in ${}
corresponds to an attribute or template function.
If you see the form’s action, you see it points to ${url.loginAction}
.
This value is automatically generated when you invoke the AuthenticationFlowContext.form() method.
You can also obtain this value by calling the AuthenticationFlowContext.getActionURL() method in Java code.
You’ll also see ${properties.someValue}
.
These correspond to properties defined in your theme.properties file of our theme.
${msg("someValue")}
corresponds to the internationalized message bundles (.properties files) included with the login theme messages/ directory.
If you’re just using english, you can just add the value of the loginSecretQuestion
.
This should be the question you want to ask the user.
When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvider instance.
If you called, LoginFormsProvider.setAttribute("foo", "bar")
, the value of "foo" would be available for reference in your form as ${foo}
.
The value of an attribute can be any Java bean as well.
If you look at the top of the file, you’ll see that we are importing a template:
<#import "select.ftl" as layout>
Importing this template, instead of the standard template.ftl
allows Keycloak to display a dropdown box that allows the user to select
a different credential or execution.
Adding an authenticator to a flow
Adding an Authenticator to a flow must be done in the Admin Console.
If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently defined flows.
You cannot modify built in flows, so, to add the Authenticator we’ve created you have to copy an existing flow or create your own.
Our hope is that the user interface is sufficiently clear so that you can determine how to create a flow and add the Authenticator. For
more details, see the Authentication Flows
chapter in Server Administration Guide .
After you’ve created your flow, you have to bind it to the login action you want to bind it to. If you go to the Authentication menu and go to the Bindings tab you will see options to bind a flow to the browser, registration, or direct grant flow.
Required action walkthrough
In this section we will discuss how to define a required action. In the Authenticator section you may have wondered, "How will we get the user’s answer to the secret question entered into the system?". As we showed in the example, if the answer is not set up, a required action will be triggered. This section discusses how to implement the required action for the Secret Question Authenticator.
Packaging classes and deployment
You will package your classes within a single jar.
This jar does not have to be separate from other provider classes but it must contain a file named org.keycloak.authentication.RequiredActionFactory
and must be contained in the META-INF/services/
directory of your jar.
This file must list the fully qualified classname of each RequiredActionFactory implementation you have in the jar.
For example:
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory
This services/ file is used by Keycloak to scan the providers it has to load into the system.
To deploy this jar, copy it to the providers/
directory, then run bin/kc.[sh|bat] build
.
Implement the RequiredActionProvider
Required actions must first implement the RequiredActionProvider interface. The RequiredActionProvider.requiredActionChallenge() is the initial call by the flow manager into the required action. This method is responsible for rendering the HTML form that will drive the required action.
@Override
public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = context.form().createForm("secret_question_config.ftl");
context.challenge(challenge);
}
You see that RequiredActionContext has similar methods to AuthenticationFlowContext. The form() method allows you to render the page from a Freemarker template. The action URL is preset by the call to this form() method. You just need to reference it within your HTML form. I’ll show you this later.
The challenge() method notifies the flow manager that a required action must be executed.
The next method is responsible for processing input from the HTML form of the required action. The action URL of the form will be routed to the RequiredActionProvider.processAction() method
@Override
public void processAction(RequiredActionContext context) {
String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer"));
UserCredentialValueModel model = new UserCredentialValueModel();
model.setValue(answer);
model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
context.getUser().updateCredentialDirectly(model);
context.success();
}
The answer is pulled out of the form post. A UserCredentialValueModel is created and the type and value of the credential are set. Then UserModel.updateCredentialDirectly() is invoked. Finally, RequiredActionContext.success() notifies the container that the required action was successful.
Implement the RequiredActionFactory
This class is really simple. It is just responsible for creating the required action provider instance.
public class SecretQuestionRequiredActionFactory implements RequiredActionFactory {
private static final SecretQuestionRequiredAction SINGLETON = new SecretQuestionRequiredAction();
@Override
public RequiredActionProvider create(KeycloakSession session) {
return SINGLETON;
}
@Override
public String getId() {
return SecretQuestionRequiredAction.PROVIDER_ID;
}
@Override
public String getDisplayText() {
return "Secret Question";
}
The getDisplayText() method is just for the Admin Console when it wants to display a friendly name for the required action.
Enable required action
The final thing you have to do is go into the Admin Console. Click on the Authentication left menu. Click on the Required Actions tab. Click on the Register button and choose your new Required Action. Your new required action should now be displayed and enabled in the required actions list.
Modifying or extending the registration form
It is entirely possible for you to implement your own flow with a set of Authenticators to totally change how registration is done in Keycloak. But what you’ll usually want to do is just add a bit of validation to the out-of-the-box registration page. An additional SPI was created to be able to do this. It basically allows you to add validation of form elements on the page as well as to initialize UserModel attributes and data after the user has been registered. We’ll look at both the implementation of the user profile registration processing as well as the registration Google reCAPTCHA Enterprise plugin.
Implementation FormAction interface
The core interface you have to implement is the FormAction interface. A FormAction is responsible for rendering and processing a portion of the page. Rendering is done in the buildPage() method, validation is done in the validate() method, post validation operations are done in success(). Let’s first take a look at buildPage() method of the Recaptcha plugin.
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
if (config == null
|| Stream.of(PROJECT_ID, SITE_KEY, API_KEY, ACTION)
.anyMatch(key -> Strings.isNullOrEmpty(config.get(key)))
|| parseDoubleFromConfig(config, SCORE_THRESHOLD) == null) {
form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
return;
}
String userLanguageTag = context.getSession().getContext().resolveLocale(context.getUser())
.toLanguageTag();
boolean invisible = Boolean.parseBoolean(config.getOrDefault(INVISIBLE, "true"));
form.setAttribute("recaptchaRequired", true);
form.setAttribute("recaptchaSiteKey", config.get(SITE_KEY));
form.setAttribute("recaptchaAction", config.get(ACTION));
form.setAttribute("recaptchaVisible", !invisible);
form.addScript("https://www.google.com/recaptcha/enterprise.js?hl=" + userLanguageTag);
}
The Recaptcha buildPage() method is a callback by the form flow to help render the page. It receives a form parameter which is a LoginFormsProvider. You can add additional attributes to the form provider so that they can be displayed in the HTML page generated by the registration Freemarker template.
The code above is from the registration recaptcha plugin. Recaptcha requires some specific settings that must be obtained from configuration. FormActions are configured in the exact same as Authenticators are. In this example, we pull the Google Recaptcha site key and other options from Recaptcha configuration and add them as attributes to the form provider. Our registration template file, register.ftl, can now have access to those attributes.
Recaptcha also has the requirement of loading a JavaScript script. You can do this by calling LoginFormsProvider.addScript(), passing in the URL.
For user profile processing, there is no additional information that it needs to add to the form, so its buildPage() method is empty.
The next meaty part of this interface is the validate() method. This is called immediately upon receiving a form post. Let’s look at the Recaptcha’s plugin first.
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
if (!Validation.isBlank(captcha) && validateRecaptcha(context, captcha)) {
context.success();
} else {
List<FormMessage> errors = new ArrayList<>();
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
context.validationError(formData, errors);
}
}
Here we obtain the form data that the Recaptcha widget adds to the form. We obtain the Recaptcha secret key from configuration. We then validate the recaptcha. If successful, ValidationContext.success() is called. We clear the captcha token from the form using formData.remove, but keep other form data untouched. If not, we invoke ValidationContext.validationError() passing in the formData (so the user doesn’t have to re-enter data), we also specify an error message we want displayed. The error message must point to a message bundle property in the internationalized message bundles. For other registration extensions validate() might be validating the format of a form element, for example an alternative email attribute.
Let’s also look at the user profile plugin that is used to validate email address and other user information when registering.
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
context.getEvent().detail(Details.REGISTER_METHOD, "form");
UserProfile profile = getOrCreateUserProfile(context, formData);
try {
profile.validate();
} catch (ValidationException pve) {
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
context.getEvent().detail(Details.EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL));
}
if (pve.hasError(Messages.EMAIL_EXISTS)) {
context.error(Errors.EMAIL_IN_USE);
} else if (pve.hasError(Messages.USERNAME_EXISTS)) {
context.error(Errors.USERNAME_IN_USE);
} else {
context.error(Errors.INVALID_REGISTRATION);
}
context.validationError(formData, errors);
return;
}
context.success();
}
As you can see, this validate() method of user profile processing makes sure that the email and all other attributes are filled in the form. It delegates to User Profile SPI, which makes sure that email is in the right format and does all other validations. If any of these validations fail, an error message is queued up for rendering. It would contain the message for every field where the validation failed.
As you can see, the user profile makes sure that registration form contains all the needed user profile fields. User profile also makes sure that correct validations
are used, attributes are correctly grouped on the page. There is a correct type used for each field (such as if a user needs to choose from predefined values), fields
are "conditionally" rendered just for some scopes (Progressive profiling) and others. So usually you will not need to implement new FormAction or registration fields, but
you can just properly configure user-profile to reflect this. For more details, see User Profile documentation.
In general, new FormAction might be useful for instance if you want to add new credentials to the registration form (such as ReCaptcha support as mentioned here) rather than new user profile fields.
|
After all validations have been processed then, the form flow then invokes the FormAction.success() method. For recaptcha this is a no-op, so we won’t go over it. For user profile processing, this method fills in values in the registered user.
@Override
public void success(FormContext context) {
checkNotOtherUserAuthenticating(context);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String email = formData.getFirst(UserModel.EMAIL);
String username = formData.getFirst(UserModel.USERNAME);
if (context.getRealm().isRegistrationEmailAsUsername()) {
username = email;
}
context.getEvent().detail(Details.USERNAME, username)
.detail(Details.REGISTER_METHOD, "form")
.detail(Details.EMAIL, email);
UserProfile profile = getOrCreateUserProfile(context, formData);
UserModel user = profile.create();
user.setEnabled(true);
// This means that following actions can retrieve user from the context by context.getUser() method
context.setUser(user);
}
The new user is created and the UserModel of the newly registered user is added to the FormContext. The appropriate methods are called to initialize UserModel data. In your own FormAction, you can possibly obtain user by using something like:
@Override
public void success(FormContext context) {
UserModel user = context.getUser();
if (user != null) {
// Do something useful with the user here ...
}
}
Finally, you are also required to define a FormActionFactory class. This class is implemented similarly to AuthenticatorFactory, so we won’t go over it.
Packaging the action
You will package your classes within a single jar.
This jar must contain a file named org.keycloak.authentication.FormActionFactory
and must be contained in the META-INF/services/
directory of your jar.
This file must list the fully qualified class name of each FormActionFactory implementation you have in the jar.
For example:
org.keycloak.authentication.forms.RegistrationUserCreation
org.keycloak.authentication.forms.RegistrationRecaptcha
This services/ file is used by Keycloak to scan the providers it has to load into the system.
To deploy this jar, copy it to the providers/
directory, then run bin/kc.[sh|bat] build
.
Adding FormAction to the registration flow
Adding a FormAction to a registration page flow must be done in the Admin Console. If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently defined flows. You cannot modify built in flows, so, to add the Authenticator we’ve created you have to copy an existing flow or create your own. I’m hoping the UI is intuitive enough so that you can figure out for yourself how to create a flow and add the FormAction.
Basically you’ll have to copy the registration flow. Then click Actions menu to the right of the Registration Form, and pick "Add execution" to add a new execution. You’ll pick the FormAction from the selection list. Make sure your FormAction comes after "Registration User Creation" by using the down buttons to move it if your FormAction isn’t already listed after "Registration User Creation". You want your FormAction to come after user creation because the success() method of Registration User Creation is responsible for creating the new UserModel.
After you’ve created your flow, you have to bind it to registration. If you go to the Authentication menu and go to the Bindings tab you will see options to bind a flow to the browser, registration, or direct grant flow.
Modifying forgot password/credential flow
Keycloak also has a specific authentication flow for forgot password, or rather credential reset initiated by a user. If you go to the Admin Console flows page, there is a "reset credentials" flow. By default, Keycloak asks for the email or username of the user and sends an email to them. If the user clicks on the link, then they are able to reset both their password and OTP (if an OTP has been set up). You can disable automatic OTP reset by disabling the "Reset OTP" authenticator in the flow.
You can add additional functionality to this flow as well. For example, many deployments would like for the user to answer one or more secret questions in additional to sending an email with a link. You could expand on the secret question example that comes with the distro and incorporate it into the reset credential flow.
One thing to note if you are extending the reset credentials flow. The first "authenticator" is just a page to obtain the username or email. If the username or email exists, then the AuthenticationFlowContext.getUser() will return the located user. Otherwise this will be null. This form WILL NOT re-ask the user to enter an email or username if the previous email or username did not exist. You need to prevent attackers from being able to guess valid users. So, if AuthenticationFlowContext.getUser() returns null, you should proceed with the flow to make it look like a valid user was selected. I suggest that if you want to add secret questions to this flow, you should ask these questions after the email is sent. In other words, add your custom authenticator after the "Send Reset Email" authenticator.
Modifying first broker login flow
First Broker Login flow is used during first login with some identity provider.
Term First Login
means that there is not yet existing Keycloak account linked with the particular authenticated identity provider account.
-
See the
Identity Brokering
chapter in Server Administration Guide .
Authentication of clients
Keycloak actually supports pluggable authentication for OpenID Connect client applications.
Authentication of client (application) is used under the hood by the Keycloak adapter during sending any backchannel requests
to the Keycloak server (like the request for exchange code to access token after successful authentication or request to refresh token).
But the client authentication can be also used directly by you during Direct Access grants
(represented by OAuth2 Resource Owner Password Credentials Flow
)
or during Service account
authentication (represented by OAuth2 Client Credentials Flow
).
-
For more details about Keycloak adapter and OAuth2 flows see Securing applications Guides.
Default implementations
Actually Keycloak has 2 default implementations of client authentication:
- Traditional authentication with client_id and client_secret
-
This is default mechanism mentioned in the OpenID Connect or OAuth2 specification and Keycloak supports it since it’s early days. The public client needs to include
client_id
parameter with its ID in the POST request (so it’s de facto not authenticated) and the confidential client needs to includeAuthorization: Basic
header with the clientId and clientSecret used as username and password. - Authentication with signed JWT
-
This is based on the JWT Bearer Token Profiles for OAuth 2.0 specification. The client/adapter generates the JWT and signs it with his private key. The Keycloak then verifies the signed JWT with the client’s public key and authenticates client based on it.
See the demo example and especially the examples/preconfigured-demo/product-app
for the example application showing
the application using client authentication with signed JWT.
Implement your own client authenticator
For plug your own client authenticator, you need to implement few interfaces on both client (adapter) and server side.
- Client side
-
Here you need to implement
org.keycloak.adapters.authentication.ClientCredentialsProvider
and put the implementation either to:-
your WAR file into WEB-INF/classes . But in this case, the implementation can be used just for this single WAR application
-
Some JAR file, which will be added into WEB-INF/lib of your WAR
-
Some JAR file, which will be used as jboss module and configured in jboss-deployment-structure.xml of your WAR. In all cases, you also need to create the file
META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider
either in the WAR or in your JAR.
-
- Server side
-
Here you need to implement
org.keycloak.authentication.ClientAuthenticatorFactory
andorg.keycloak.authentication.ClientAuthenticator
. You also need to add the fileMETA-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
with the name of the implementation classes. See authenticators for more details.
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 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.
Keycloak offers four basic token types allowing the bearer to:
-
Reset credentials
-
Confirm e-mail address
-
Execute required action(s)
-
Confirm linking of an account with account in external identity provider
In addition to that, it is possible to implement any functionality that initiates or modifies authentication session using action token handler SPI, details of which are described in the text below.
Anatomy of action token
Action token is a standard Json Web Token signed with active realm key where the payload contains several fields:
-
typ
- Identification of the action (e.g.verify-email
) -
iat
andexp
- Times of token validity -
sub
- ID of the user -
azp
- Client name -
iss
- Issuer - URL of the issuing realm -
aud
- Audience - list containing URL of the issuing realm -
asid
- ID of the authentication session (optional) -
nonce
- Random nonce to guarantee uniqueness of use if the operation can only be executed once (optional)
In addition, an action token can contain any number of custom fields serializable into JSON.
Action token processing
When an action token is passed to a Keycloak endpoint
KEYCLOAK_ROOT/realms/master/login-actions/action-token
via key
parameter, it is validated and a proper action
token handler is executed. The processing always takes place in a context of an authentication session, either a fresh
one or the action token service joins an existing authentication session (details are described below). The action token
handler can perform actions prescribed by the token (often it alters the authentication session) and results into an HTTP
response (e.g. it can continue in authentication or display an information/error page). These steps are detailed below.
-
Basic action token validation. Signature and time validity is checked, and action token handler is determined based on
typ
field. -
Determining authentication session. If the action token URL was opened in browser with existing authentication session, and the token contains authentication session ID matching the authentication session from the browser, action token validation and handling will attach this ongoing authentication session. Otherwise, action token handler creates a fresh authentication session that replaces any other authentication session present at that time in the browser.
-
Token validations specific for token type. Action token endpoint logic validates that the user (
sub
field) and client (azp
) from the token exist, are valid and not disabled. Then it validates all custom validations defined in the action token handler. Furthermore, token handler can request this token be single-use. Already used tokens would then be rejected by action token endpoint logic. -
Performing the action. After all these validations, action token handler code is called that performs the actual action according to parameters in the token.
-
Invalidation of single-Use tokens. If the token is set to single-use, once the authentication flow finishes, the action token is invalidated.
Implement your own action token and its handler
How to create an action token
As action token is just a signed JWT with few mandatory fields (see Anatomy of action token
above), it can be serialized and signed as such using Keycloak’s JWSBuilder
class. This way has been already
implemented in serialize(session, realm, uriInfo)
method of org.keycloak.authentication.actiontoken.DefaultActionToken
and can be leveraged by implementers by using that class for tokens instead of plain JsonWebToken
.
The following example shows the implementation of a simple action token. Note that the class must have a private constructor without any arguments. This is necessary to deserialize the token class from JWT.
import org.keycloak.authentication.actiontoken.DefaultActionToken;
public class DemoActionToken extends DefaultActionToken {
public static final String TOKEN_TYPE = "my-demo-token";
public DemoActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
}
private DemoActionToken() {
// Required to deserialize from JWT
super();
}
}
If the action token you are implementing contains any custom fields that should be serializabled to JSON fields, you
should consider implementing a descendant of org.keycloak.representations.JsonWebToken
class that would implement
org.keycloak.models.ActionTokenKeyModel
interface. In that case, you can take advantage of the existing
org.keycloak.authentication.actiontoken.DefaultActionToken
class as it already satisfies both these conditions,
and either use it directly or implement its child, the fields of which can be annotated with appropriate Jackson
annotations, e.g. com.fasterxml.jackson.annotation.JsonProperty
to serialize them to JSON.
The following example extends the DemoActionToken
from the previous example with the field demo-id
:
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.authentication.actiontoken.DefaultActionToken;
public class DemoActionToken extends DefaultActionToken {
public static final String TOKEN_TYPE = "my-demo-token";
private static final String JSON_FIELD_DEMO_ID = "demo-id";
@JsonProperty(value = JSON_FIELD_DEMO_ID)
private String demoId;
public DemoActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId, String demoId) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
this.demoId = demoId;
}
private DemoActionToken() {
// you must have this private constructor for deserializer
}
public String getDemoId() {
return demoId;
}
}
Packaging classes and deployment
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 oftyp
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 arbitrary. This method is only called if all verifiers (including those defined ingetVerifiers(context)
) have succeeded, and it is guaranteed that thetoken
would be of the class returned bygetTokenClass()
method.To be able to determine whether the action token was issued for the current authentication session as described in Item 2 above, method for extracting authentication session ID has to be declared in
getAuthenticationSessionIdFromToken(token, context)
method. The implementation inDefaultActionToken
returns the value ofasid
field from the token if it is defined. Note that you can override that method to return current authentication session ID regardless of the token - that way you can create tokens that would step into the ongoing authentication flow before any authentication flow would be started.If the authentication session from the token does not match the current one, the action token handler would be asked to start a fresh one by calling
startFreshAuthenticationSession(token, context)
. It can throw aVerificationException
(or better its more descriptive variantExplainedTokenVerificationException
) to signal that would be forbidden.The token handler also determines via method
canUseTokenRepeatedly(token, context)
whether the token would be invalidated after it is used and authentication completes. Note that if you would have a flow utilizing multiple action token, only the last token would be invalidated. In that case, you should useorg.keycloak.models.SingleUseObjectProvider
in action token handler to invalidate the used tokens manually.Default implementation of most of the
ActionTokenHandler
methods is theorg.keycloak.authentication.actiontoken.AbstractActionTokenHandler
abstract class inkeycloak-services
module. The only method that needs to be implemented ishandleToken(token, context)
that performs the actual action. -
org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
- factory that instantiates action token handler. Implementations have to overridegetId()
to return value that must match precisely the value oftyp
field in the action token.Note that you have to register the custom
ActionTokenHandlerFactory
implementation as explained in the Service Provider Interfaces section of this guide.
Event Listener SPI
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.
For details on how to package and deploy a custom provider refer to the Service Provider Interfaces chapter.
SAML role mappings SPI
Keycloak defines an SPI for mapping SAML roles into roles that exist in the SP environment. The roles returned by a third-party IDP might not always correspond to the roles that were defined for the SP application so there is a need for a mechanism that allows mapping the SAML roles into different roles. It is used by the SAML adapter after it extracts the roles from the SAML assertion to set up the container’s security context.
The org.keycloak.adapters.saml.RoleMappingsProvider
SPI doesn’t impose any restrictions on the mappings that can be performed.
Implementations can not only map roles into other roles but also add or remove roles (and thus augment or reduce the set of
roles assigned to the SAML principal) depending on the use case.
For details about the configuration of the role mappings provider for the SAML adapter as well as a description of the default implementations available see the Securing applications Guides.
Implementing a custom role mappings provider
To implement a custom role mappings provider one first needs to implement the org.keycloak.adapters.saml.RoleMappingsProvider
interface. Then, a META-INF/services/org.keycloak.adapters.saml.RoleMappingsProvider
file containing the fully qualified name
of the custom implementation must be added to the archive that also contains the implementation class. This archive can be:
-
The SP application WAR file where the provider class is included in WEB-INF/classes;
-
A custom JAR file which will be added into WEB-INF/lib of the SP application WAR;
-
(WildFly/JBoss EAP only) A custom JAR file configured as a
jboss module
and referenced injboss-deployment-structure.xml
of the SP application WAR.
When the SP application is deployed, the role mappings provider that will be used is selected by the id that was set in
keycloak-saml.xml
or in the keycloak-saml
subsystem. So to enable your custom provider simply make sure that its id is
properly set in the adapter configuration.
User Storage SPI
You can use the User Storage SPI to write extensions to Keycloak to connect to external user databases and credential stores. The built-in LDAP and ActiveDirectory support is an implementation of this SPI in action. Out of the box, Keycloak uses its local database to create, update, and look up users and validate credentials. Often though, organizations have existing external proprietary user databases that they cannot migrate to Keycloak’s data model. For those situations, application developers can write implementations of the User Storage SPI to bridge the external user store and the internal user object model that Keycloak uses to log in users and manage them.
When the Keycloak runtime needs to look up a user, such as when a user is logging in, it performs a number of steps to locate the user. It first looks to see if the user is in the user cache; if the user is found it uses that in-memory representation. Then it looks for the user within the Keycloak local database. If the user is not found, it then loops through User Storage SPI provider implementations to perform the user query until one of them returns the user the runtime is looking for. The provider queries the external user store for the user and maps the external data representation of the user to Keycloak’s user metamodel.
User Storage SPI provider implementations can also perform complex criteria queries, perform CRUD operations on users, validate and manage credentials, or perform bulk updates of many users at once. It depends on the capabilities of the external store.
User Storage SPI provider implementations are packaged and deployed similarly to (and often are) Jakarta EE components. They are not enabled by default, but instead must be enabled and configured per realm under the User Federation
tab in the administration console.
If your user provider implementation is using some user attributes as the metadata attributes for linking/establishing the user identity,
then please make sure that users are not able to edit the attributes and the corresponding attributes are read-only. The example is the LDAP_ID attribute, which the built-in Keycloak
LDAP provider is using for to store the ID of the user on the LDAP server side. See the details in the Threat model mitigation chapter.
|
There are two sample projects in Keycloak Quickstarts Repository. Each quickstart has a README
file with instructions on how to build, deploy, and test the sample project. The following table provides a brief description of the available User Storage SPI quickstarts:
Name | Description |
---|---|
Demonstrates implementing a user storage provider using JPA. |
|
Demonstrates implementing a user storage provider using a simple properties file that contains username/password key pairs. |
Provider interfaces
When building an implementation of the User Storage SPI you have to define a provider class and a provider factory.
Provider class instances are created per transaction by provider factories.
Provider classes do all the heavy lifting of user lookup and other user operations. They must implement the
org.keycloak.storage.UserStorageProvider
interface.
package org.keycloak.storage;
public interface UserStorageProvider extends Provider {
/**
* Callback when a realm is removed. Implement this if, for example, you want to do some
* cleanup in your user storage when a realm is removed
*
* @param realm
*/
default
void preRemove(RealmModel realm) {
}
/**
* Callback when a group is removed. Allows you to do things like remove a user
* group mapping in your external store if appropriate
*
* @param realm
* @param group
*/
default
void preRemove(RealmModel realm, GroupModel group) {
}
/**
* Callback when a role is removed. Allows you to do things like remove a user
* role mapping in your external store if appropriate
* @param realm
* @param role
*/
default
void preRemove(RealmModel realm, RoleModel role) {
}
}
You may be thinking that the UserStorageProvider
interface is pretty sparse? You’ll see later in this chapter that there are other mix-in interfaces your provider class may implement to support the meat of user integration.
UserStorageProvider
instances are created once per transaction. When the transaction is complete, the UserStorageProvider.close()
method is invoked and the instance is then garbage collected. Instances are created by provider factories. Provider factories implement the org.keycloak.storage.UserStorageProviderFactory
interface.
package org.keycloak.storage;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserStorageProviderFactory<T extends UserStorageProvider> extends ComponentFactory<T, UserStorageProvider> {
/**
* This is the name of the provider and will be shown in the admin console as an option.
*
* @return
*/
@Override
String getId();
/**
* called per Keycloak transaction.
*
* @param session
* @param model
* @return
*/
T create(KeycloakSession session, ComponentModel model);
...
}
Provider factory classes must specify the concrete provider class as a template parameter when implementing the
UserStorageProviderFactory
. This is a must as the runtime will introspect this class to scan for its capabilities
(the other interfaces it implements). So for example, if your provider class is named FileProvider
, then the
factory class should look like this:
public class FileProviderFactory implements UserStorageProviderFactory<FileProvider> {
public String getId() { return "file-provider"; }
public FileProvider create(KeycloakSession session, ComponentModel model) {
...
}
The getId()
method returns the name of the User Storage provider. This id will be displayed in the admin console’s
User Federation page when you want to enable the provider for a specific realm.
The create()
method is responsible for allocating an instance of the provider class. It takes a org.keycloak.models.KeycloakSession
parameter. This object can be used to look up other information and metadata as well as provide access to various other
components within the runtime. The ComponentModel
parameter represents how the provider was enabled and configured within
a specific realm. It contains the instance id of the enabled provider as well as any configuration you may have specified
for it when you enabled through the admin console.
The UserStorageProviderFactory
has other capabilities as well which we will go over later in this chapter.
Provider capability interfaces
If you have examined the UserStorageProvider
interface closely you might notice that it does not define any methods for locating or managing users. These methods are actually defined in other capability interfaces depending on what scope of capabilities your external user store can provide and execute on. For example, some external stores are read-only and can only do simple queries and credential validation. You will only be required to implement the capability interfaces for the features you are able to. You can implement these interfaces:
SPI | Description |
---|---|
|
This interface is required if you want to be able to log in with users from this external store. Most (all?) providers implement this interface. |
|
Defines complex queries that are used to locate one or more users. You must implement this interface if you want to view and manage users from the administration console. |
|
Implement this interface if your provider supports count queries. |
|
This interface is combined capability of |
|
Implement this interface if your provider supports adding and removing users. |
|
Implement this interface if your provider supports bulk update of a set of users. |
|
Implement this interface if your provider can validate one or more different credential types (for example, if your provider can validate a password). |
|
Implement this interface if your provider supports updating one or more different credential types. |
Model interfaces
Most of the methods defined in the capability interfaces either return or are passed in representations of a user. These representations are defined by the org.keycloak.models.UserModel
interface. App developers are required to implement this interface. It provides a mapping between the external user store and the user metamodel that Keycloak uses.
package org.keycloak.models;
public interface UserModel extends RoleMapperModel {
String getId();
String getUsername();
void setUsername(String username);
String getFirstName();
void setFirstName(String firstName);
String getLastName();
void setLastName(String lastName);
String getEmail();
void setEmail(String email);
...
}
UserModel
implementations provide access to read and update metadata about the user including things like username, name, email, role and group mappings, as well as other arbitrary attributes.
There are other model classes within the org.keycloak.models
package that represent other parts of the Keycloak metamodel: RealmModel
, RoleModel
, GroupModel
, and ClientModel
.
Storage Ids
One important method of UserModel
is the getId()
method. When implementing UserModel
developers must be aware of the user id format. The format must be:
"f:" + component id + ":" + external id
The Keycloak runtime often has to look up users by their user id. The user id contains enough information so that the runtime does not have to query every single UserStorageProvider
in the system to find the user.
The component id is the id returned from ComponentModel.getId()
. The ComponentModel
is passed in as a parameter when creating the provider class so you can get it from there. The external id is information your provider class needs to find the user in the external store. This is often a username or a uid. For example, it might look something like this:
f:332a234e31234:wburke
When the runtime does a lookup by id, the id is parsed to obtain the component id. The component id is used to locate the UserStorageProvider
that was originally used to load the user. That provider is then passed the id. The provider again parses the id to obtain the external id and it will use to locate the user in external user storage.
Packaging and deployment
In order for Keycloak to recognize the provider, you need to add a file to the JAR: META-INF/services/org.keycloak.storage.UserStorageProviderFactory
. This file must contain a line-separated list of fully qualified classnames of the UserStorageProviderFactory
implementations:
org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
To deploy this jar, copy it to the providers/
directory, then run bin/kc.[sh|bat] build
.
Simple read-only, lookup example
To illustrate the basics of implementing the User Storage SPI let’s walk through a simple example. In this chapter you’ll see the implementation of a simple UserStorageProvider
that looks up users in a simple property file. The property file contains username and password definitions and is hardcoded to a specific location on the classpath. The provider will be able to look up the user by ID and username and also be able to validate passwords. Users that originate from this provider will be read-only.
Provider class
The first thing we will walk through is the UserStorageProvider
class.
public class PropertyFileUserStorageProvider implements
UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
CredentialInputUpdater
{
...
}
Our provider class, PropertyFileUserStorageProvider
, implements many interfaces. It implements the UserStorageProvider
as that is a base requirement of the SPI. It implements the UserLookupProvider
interface because we want to be able to log in with users stored by this provider. It implements the CredentialInputValidator
interface because we want to be able to validate passwords entered in using the login screen. Our property file is read-only. We implement the CredentialInputUpdater
because we want to post an error condition when the user attempts to update his password.
protected KeycloakSession session;
protected Properties properties;
protected ComponentModel model;
// map of loaded users in this transaction
protected Map<String, UserModel> loadedUsers = new HashMap<>();
public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
this.session = session;
this.model = model;
this.properties = properties;
}
The constructor for this provider class is going to store the reference to the KeycloakSession
, ComponentModel
, and property file. We’ll use all of these later. Also notice that there is a map of loaded users. Whenever we find a user we will store it in this map so that we avoid re-creating it again within the same transaction. This is a good practice to follow as many providers will need to do this (that is, any provider that integrates with JPA). Remember also that provider class instances are created once per transaction and are closed after the transaction completes.
UserLookupProvider implementation
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
UserModel adapter = loadedUsers.get(username);
if (adapter == null) {
String password = properties.getProperty(username);
if (password != null) {
adapter = createAdapter(realm, username);
loadedUsers.put(username, adapter);
}
}
return adapter;
}
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapter(session, realm, model) {
@Override
public String getUsername() {
return username;
}
};
}
@Override
public UserModel getUserById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
String username = storageId.getExternalId();
return getUserByUsername(realm, username);
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
return null;
}
The getUserByUsername()
method is invoked by the Keycloak login page when a user logs in. In our implementation we first check the loadedUsers
map to see if the user has already been loaded within this transaction. If it hasn’t been loaded we look in the property file for the username. If it exists we create an implementation of UserModel
, store it in loadedUsers
for future reference, and return this instance.
The createAdapter()
method uses the helper class org.keycloak.storage.adapter.AbstractUserAdapter
. This provides a base implementation for UserModel
. It automatically generates a user id based on the required storage id format using the username of the user as the external id.
"f:" + component id + ":" + username
Every get method of AbstractUserAdapter
either returns null or empty collections. However, methods that return role and group mappings will return the default roles and groups configured for the realm for every user. Every set method of AbstractUserAdapter
will throw a org.keycloak.storage.ReadOnlyException
. So if you attempt to modify the user in the Admin Console, you will get an error.
The getUserById()
method parses the id
parameter using the org.keycloak.storage.StorageId
helper class. The StorageId.getExternalId()
method is invoked to obtain the username embedded in the id
parameter. The method then delegates to getUserByUsername()
.
Emails are not stored, so the getUserByEmail()
method returns null.
CredentialInputValidator implementation
Next let’s look at the method implementations for CredentialInputValidator
.
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
String password = properties.getProperty(user.getUsername());
return credentialType.equals(PasswordCredentialModel.TYPE) && password != null;
}
@Override
public boolean supportsCredentialType(String credentialType) {
return credentialType.equals(PasswordCredentialModel.TYPE);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType())) return false;
String password = properties.getProperty(user.getUsername());
if (password == null) return false;
return password.equals(input.getChallengeResponse());
}
The isConfiguredFor()
method is called by the runtime to determine if a specific credential type is configured for the user. This method checks to see that the password is set for the user.
The supportsCredentialType()
method returns whether validation is supported for a specific credential type. We check to see if the credential type is password
.
The isValid()
method is responsible for validating passwords. The CredentialInput
parameter is really just an abstract interface for all credential types. We make sure that we support the credential type and also that it is an instance of UserCredentialModel
. When a user logs in through the login page, the plain text of the password input is put into an instance of UserCredentialModel
. The isValid()
method checks this value against the plain text password stored in the properties file. A return value of true
means the password is valid.
CredentialInputUpdater implementation
As noted before, the only reason we implement the CredentialInputUpdater
interface in this example is to forbid modifications of user passwords. The reason we have to do this is because otherwise the runtime would allow the password to be overridden in Keycloak local storage. We’ll talk more about this later in this chapter.
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (input.getType().equals(PasswordCredentialModel.TYPE)) throw new ReadOnlyException("user is read only for this update");
return false;
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
}
@Override
public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
return Stream.empty();
}
The updateCredential()
method just checks to see if the credential type is password. If it is, a ReadOnlyException
is thrown.
Provider factory implementation
Now that the provider class is complete, we now turn our attention to the provider factory class.
public class PropertyFileUserStorageProviderFactory
implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
public static final String PROVIDER_NAME = "readonly-property-file";
@Override
public String getId() {
return PROVIDER_NAME;
}
First thing to notice is that when implementing the UserStorageProviderFactory
class, you must pass in the concrete provider class implementation as a template parameter. Here we specify the provider class we defined before: PropertyFileUserStorageProvider
.
If you do not specify the template parameter, your provider will not function. The runtime does class introspection to determine the capability interfaces that the provider implements. |
The getId()
method identifies the factory in the runtime and will also be the string shown in the admin console when you want to enable a user storage provider for the realm.
Initialization
private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class);
protected Properties properties = new Properties();
@Override
public void init(Config.Scope config) {
InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties");
if (is == null) {
logger.warn("Could not find users.properties in classpath");
} else {
try {
properties.load(is);
} catch (IOException ex) {
logger.error("Failed to load users.properties file", ex);
}
}
}
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new PropertyFileUserStorageProvider(session, model, properties);
}
The UserStorageProviderFactory
interface has an optional init()
method you can implement. When Keycloak boots up, only one instance of each provider factory is created. Also at boot time, the init()
method is called on each of these factory instances. There’s also a postInit()
method you can implement as well. After each factory’s init()
method is invoked, their postInit()
methods are called.
In our init()
method implementation, we find the property file containing our user declarations from the classpath. We then load the properties
field with the username and password combinations stored there.
The Config.Scope
parameter is factory configuration that configured through server configuration.
For example, by running the server with the following argument:
kc.[sh|bat] start --spi-storage-readonly-property-file-path=/other-users.properties
We can specify the classpath of the user property file instead of hardcoding it. Then you can retrieve the configuration in the PropertyFileUserStorageProviderFactory.init()
:
public void init(Config.Scope config) {
String path = config.get("path");
InputStream is = getClass().getClassLoader().getResourceAsStream(path);
...
}
Create method
Our last step in creating the provider factory is the create()
method.
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new PropertyFileUserStorageProvider(session, model, properties);
}
We simply allocate the PropertyFileUserStorageProvider
class. This create method will be called once per transaction.
Packaging and deployment
The class files for our provider implementation should be placed in a jar. You also have to declare the provider factory class within the META-INF/services/org.keycloak.storage.UserStorageProviderFactory
file.
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
To deploy this jar, copy it to the providers/
directory, then run bin/kc.[sh|bat] build
.
Enabling the provider in the Admin Console
You enable user storage providers per realm within the User Federation page in the Admin Console.
-
Select the provider we just created from the list:
readonly-property-file
.The configuration page for our provider displays.
-
Click Save because we have nothing to configure.
Configured Provider -
Return to the main User Federation page
You now see your provider listed.
User Federation
You will now be able to log in with a user declared in the users.properties
file. This user will only be able to view the account page after logging in.
Configuration techniques
Our PropertyFileUserStorageProvider
example is a bit contrived. It is hardcoded to a property file that is embedded in the jar of the provider, which is not terribly useful. We might want to make the location of this file configurable per instance of the provider. In other words, we might want to reuse this provider multiple times in multiple different realms and point to completely different user property files. We’ll also want to perform this configuration within the Admin Console UI.
The UserStorageProviderFactory
has additional methods you can implement that handle provider configuration. You describe the variables you want to configure per provider and the Admin Console automatically renders a generic input page to gather this configuration. When implemented, callback methods also validate the configuration before it is saved, when a provider is created for the first time, and when it is updated. UserStorageProviderFactory
inherits these methods from the org.keycloak.component.ComponentFactory
interface.
List<ProviderConfigProperty> getConfigProperties();
default
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
throws ComponentValidationException
{
}
default
void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
default
void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
The ComponentFactory.getConfigProperties()
method returns a list of org.keycloak.provider.ProviderConfigProperty
instances. These instances declare metadata that is needed to render and store each configuration variable of the provider.
Configuration example
Let’s expand our PropertyFileUserStorageProviderFactory
example to allow you to point a provider instance to a specific file on disk.
public class PropertyFileUserStorageProviderFactory
implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
protected static final List<ProviderConfigProperty> configMetadata;
static {
configMetadata = ProviderConfigurationBuilder.create()
.property().name("path")
.type(ProviderConfigProperty.STRING_TYPE)
.label("Path")
.defaultValue("${jboss.server.config.dir}/example-users.properties")
.helpText("File path to properties file")
.add().build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configMetadata;
}
The ProviderConfigurationBuilder
class is a great helper class to create a list of configuration properties. Here we specify a variable named path
that is a String type. On the Admin Console configuration page for this provider, this configuration variable is labeled as Path
and has a default value of ${jboss.server.config.dir}/example-users.properties
. When you hover over the tooltip of this configuration option, it displays the help text, File path to properties file
.
The next thing we want to do is to verify that this file exists on disk. We do not want to enable an instance of this provider in the realm unless it points to a valid user property file. To do this, we implement the validateConfiguration()
method.
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
throws ComponentValidationException {
String fp = config.getConfig().getFirst("path");
if (fp == null) throw new ComponentValidationException("user property file does not exist");
fp = EnvUtil.replace(fp);
File file = new File(fp);
if (!file.exists()) {
throw new ComponentValidationException("user property file does not exist");
}
}
The validateConfiguration()
method provides the configuration variable from the ComponentModel
to verify if that file exists on disk.
Notice that the use of the org.keycloak.common.util.EnvUtil.replace()
method. With this method any string that includes ${}
will replace that value with a system property value.
The ${jboss.server.config.dir}
string corresponds to the conf/
directory of our server and is really useful for this example.
Next thing we have to do is remove the old init()
method. We do this because user property files are going to be unique per provider instance. We move this logic to the create()
method.
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
String path = model.getConfig().getFirst("path");
Properties props = new Properties();
try {
InputStream is = new FileInputStream(path);
props.load(is);
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
return new PropertyFileUserStorageProvider(session, model, props);
}
This logic is, of course, inefficient as every transaction reads the entire user property file from disk, but hopefully this illustrates, in a simple way, how to hook in configuration variables.
Add/Remove user and query capability interfaces
One thing we have not done with our example is allow it to add and remove users or change passwords. Users defined in our example are
also not queryable or viewable in the Admin Console. To add these enhancements, our example provider must implement
the UserQueryMethodsProvider
(or UserQueryProvider
) and UserRegistrationProvider
interfaces.
Implementing UserRegistrationProvider
Use this procedure to implement adding and removing users from the particular store, we first have to be able to save our properties file to disk.
public void save() {
String path = model.getConfig().getFirst("path");
path = EnvUtil.replace(path);
try {
FileOutputStream fos = new FileOutputStream(path);
properties.store(fos, "");
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Then, the implementation of the addUser()
and removeUser()
methods becomes simple.
public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD";
@Override
public UserModel addUser(RealmModel realm, String username) {
synchronized (properties) {
properties.setProperty(username, UNSET_PASSWORD);
save();
}
return createAdapter(realm, username);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
synchronized (properties) {
if (properties.remove(user.getUsername()) == null) return false;
save();
return true;
}
}
Notice that when adding a user we set the password value of the property map to be UNSET_PASSWORD
. We do this as
we can’t have null values for a property in the property value. We also have to modify the CredentialInputValidator
methods to reflect this.
The addUser()
method will be called if the provider implements the UserRegistrationProvider
interface. If your provider has
a configuration switch to turn off adding a user, returning null
from this method will skip the provider and call
the next one.
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
String password = properties.getProperty(user.getUsername());
if (password == null || UNSET_PASSWORD.equals(password)) return false;
return password.equals(cred.getValue());
}
Since we can now save our property file, it also makes sense to allow password updates.
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
if (!input.getType().equals(PasswordCredentialModel.TYPE)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
synchronized (properties) {
properties.setProperty(user.getUsername(), cred.getValue());
save();
}
return true;
}
We can now also implement disabling a password.
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (!credentialType.equals(PasswordCredentialModel.TYPE)) return;
synchronized (properties) {
properties.setProperty(user.getUsername(), UNSET_PASSWORD);
save();
}
}
private static final Set<String> disableableTypes = new HashSet<>();
static {
disableableTypes.add(PasswordCredentialModel.TYPE);
}
@Override
public Stream<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return disableableTypes.stream();
}
With these methods implemented, you’ll now be able to change and disable the password for the user in the Admin Console.
Implementing UserQueryProvider
UserQueryProvider
is combination of UserQueryMethodsProvider
and UserCountMethodsProvider
. Without implementing UserQueryMethodsProvider
the Admin Console would not be able to view and manage users that were loaded
by our example provider. Let’s look at implementing this interface.
@Override
public int getUsersCount(RealmModel realm) {
return properties.size();
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
Predicate<String> predicate = "*".equals(search) ? username -> true : username -> username.contains(search);
return properties.keySet().stream()
.map(String.class::cast)
.filter(predicate)
.skip(firstResult)
.map(username -> getUserByUsername(realm, username))
.limit(maxResults);
}
The first declaration of searchForUserStream()
takes a String
parameter. In this example, the parameter represents a username that you want to search by. This string can be a substring, which explains the choice of the String.contains()
method when doing the search. Notice the use of *
to indicate to request a list of all users.
The method iterates over the key set of the property file, delegating to getUserByUsername()
to load a user.
Notice that we are indexing this call based on the firstResult
and maxResults
parameter. If your external store does not support pagination, you will have to do similar logic.
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
// only support searching by username
String usernameSearchString = params.get("username");
if (usernameSearchString != null)
return searchForUserStream(realm, usernameSearchString, firstResult, maxResults);
// if we are not searching by username, return all users
return searchForUserStream(realm, "*", firstResult, maxResults);
}
The searchForUserStream()
method that takes a Map
parameter can search for a user based on first, last name, username, and email.
Only usernames are stored, so the search is based only on usernames except when the Map
parameter does not contain the username
attribute.
In this case, all users are returned. In that situation, the searchForUserStream(realm, search, firstResult, maxResults)
is used.
@Override
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
return Stream.empty();
}
@Override
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
return Stream.empty();
}
Groups or attributes are not stored, so the other methods return an empty stream.
Augmenting external storage
The PropertyFileUserStorageProvider
example is really limited. While we will be able to log in with users stored
in a property file, we won’t be able to do much else. If users loaded by this provider need special role or group
mappings to fully access particular applications there is no way for us to add additional role mappings to these users.
You also can’t modify or add additional important attributes like email, first and last name.
For these types of situations, Keycloak allows you to augment your external store by storing extra information
in Keycloak’s database. This is called federated user storage and is encapsulated within the
org.keycloak.storage.federated.UserFederatedStorageProvider
class.
package org.keycloak.storage.federated;
public interface UserFederatedStorageProvider extends Provider,
UserAttributeFederatedStorage,
UserBrokerLinkFederatedStorage,
UserConsentFederatedStorage,
UserNotBeforeFederatedStorage,
UserGroupMembershipFederatedStorage,
UserRequiredActionsFederatedStorage,
UserRoleMappingsFederatedStorage,
UserFederatedUserCredentialStore {
...
}
The UserFederatedStorageProvider
instance is available on the UserStorageUtil.userFederatedStorage(KeycloakSession)
method.
It has all different kinds of methods for storing attributes, group and role mappings, different credential types,
and required actions. If your external store’s datamodel cannot support the full Keycloak feature
set, then this service can fill in the gaps.
Keycloak comes with a helper class org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage
that will delegate every single UserModel
method except get/set of username to user federated storage. Override
the methods you need to override to delegate to your external storage representations. It is strongly
suggested you read the javadoc of this class as it has smaller protected methods you may want to override. Specifically
surrounding group membership and role mappings.
Augmentation example
In our PropertyFileUserStorageProvider
example, we just need a simple change to our provider to use the
AbstractUserAdapterFederatedStorage
.
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapterFederatedStorage(session, realm, model) {
@Override
public String getUsername() {
return username;
}
@Override
public void setUsername(String username) {
String pw = (String)properties.remove(username);
if (pw != null) {
properties.put(username, pw);
save();
}
}
};
}
We instead define an anonymous class implementation of AbstractUserAdapterFederatedStorage
. The setUsername()
method makes changes to the properties file and saves it.
Import implementation strategy
When implementing a user storage provider, there’s another strategy you can take. Instead of using user federated storage, you can create a user locally in the Keycloak built-in user database and copy attributes from your external store into this local copy. There are many advantages to this approach.
-
Keycloak basically becomes a persistence user cache for your external store. Once the user is imported you’ll no longer hit the external store thus taking load off of it.
-
If you are moving to Keycloak as your official user store and deprecating the old external store, you can slowly migrate applications to use Keycloak. When all applications have been migrated, unlink the imported user, and retire the old legacy external store.
There are some obvious disadvantages though to using an import strategy:
-
Looking up a user for the first time will require multiple updates to Keycloak database. This can be a big performance loss under load and put a lot of strain on the Keycloak database. The user federated storage approach will only store extra data as needed and may never be used depending on the capabilities of your external store.
-
With the import approach, you have to keep local Keycloak storage and external storage in sync. The User Storage SPI has capability interfaces that you can implement to support synchronization, but this can quickly become painful and messy.
To implement the import strategy you simply check to see first if the user has been imported locally. If so return the local user, if not create the user locally and import data from the external store. You can also proxy the local user so that most changes are automatically synchronized.
This will be a bit contrived, but we can extend our PropertyFileUserStorageProvider
to take this approach. We
begin first by modifying the createAdapter()
method.
protected UserModel createAdapter(RealmModel realm, String username) {
UserModel local = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, username);
if (local == null) {
local = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, username);
local.setFederationLink(model.getId());
}
return new UserModelDelegate(local) {
@Override
public void setUsername(String username) {
String pw = (String)properties.remove(username);
if (pw != null) {
properties.put(username, pw);
save();
}
super.setUsername(username);
}
};
}
In this method we call the UserStoragePrivateUtil.userLocalStorage(session)
method to obtain a reference to local Keycloak
user storage. We see if the user is stored locally, if not, we add it locally. Do not set the id
of the local user.
Let Keycloak automatically generate the id
. Also note that we call
UserModel.setFederationLink()
and pass in the ID of the ComponentModel
of our provider. This sets a link between
the provider and the imported user.
When a user storage provider is removed, any user imported by it will also be removed. This is one of the
purposes of calling UserModel.setFederationLink() .
|
Another thing to note is that if a local user is linked, your storage provider will still be delegated to for methods
that it implements from the CredentialInputValidator
and CredentialInputUpdater
interfaces. Returning false
from a validation or update will just result in Keycloak seeing if it can validate or update using
local storage.
Also notice that we are proxying the local user using the org.keycloak.models.utils.UserModelDelegate
class.
This class is an implementation of UserModel
. Every method just delegates to the UserModel
it was instantiated with.
We override the setUsername()
method of this delegate class to synchronize automatically with the property file.
For your providers, you can use this to intercept other methods on the local UserModel
to perform synchronization
with your external store. For example, get methods could make sure that the local store is in sync. Set methods
keep the external store in sync with the local one. One thing to note is that the getId()
method should always return
the id that was auto generated when you created the user locally. You should not return a federated id as shown in
the other non-import examples.
If your provider is implementing the UserRegistrationProvider interface, your removeUser() method does not
need to remove the user from local storage. The runtime will automatically perform this operation. Also
note that removeUser() will be invoked before it is removed from local storage.
|
ImportedUserValidation interface
If you remember earlier in this chapter, we discussed how querying for a user worked. Local storage is queried first,
if the user is found there, then the query ends. This is a problem for our above implementation as we want
to proxy the local UserModel
so that we can keep usernames in sync. The User Storage SPI has a callback for whenever
a linked local user is loaded from the local database.
package org.keycloak.storage.user;
public interface ImportedUserValidation {
/**
* If this method returns null, then the user in local storage will be removed
*
* @param realm
* @param user
* @return null if user no longer valid
*/
UserModel validate(RealmModel realm, UserModel user);
}
Whenever a linked local user is loaded, if the user storage provider class implements this interface, then the
validate()
method is called. Here you can proxy the local user passed in as a parameter and return it. That
new UserModel
will be used. You can also optionally do a check to see if the user still exists in the external store.
If validate()
returns null
, then the local user will be removed from the database.
ImportSynchronization interface
With the import strategy you can see that it is possible for the local user copy to get out of sync with
external storage. For example, maybe a user has been removed from the external store. The User Storage SPI has
an additional interface you can implement to deal with this, org.keycloak.storage.user.ImportSynchronization
:
package org.keycloak.storage.user;
public interface ImportSynchronization {
SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
}
This interface is implemented by the provider factory. Once this interface is implemented by the provider factory, the administration console management page for the provider shows additional options. You can manually force a synchronization by clicking a button. This invokes the ImportSynchronization.sync()
method. Also, additional configuration options are displayed that allow you to automatically schedule a synchronization. Automatic synchronizations invoke the syncSince()
method.
User caches
When a user object is loaded by ID, username, or email queries it is cached. When a user object is being cached, it iterates through
the entire UserModel
interface and pulls this information to a local in-memory-only cache. In a cluster, this cache
is still local, but it becomes an invalidation cache. When a user object is modified, it is evicted. This eviction event
is propagated to the entire cluster so that the other nodes' user cache is also invalidated.
Managing the user cache
You can access the user cache by calling KeycloakSession.getProvider(UserCache.class)
.
/**
* All these methods effect an entire cluster of Keycloak instances.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserCache extends UserProvider {
/**
* Evict user from cache.
*
* @param user
*/
void evict(RealmModel realm, UserModel user);
/**
* Evict users of a specific realm
*
* @param realm
*/
void evict(RealmModel realm);
/**
* Clear cache entirely.
*
*/
void clear();
}
There are methods for evicting specific users, users contained in a specific realm, or the entire cache.
OnUserCache callback interface
You might want to cache additional information that is specific to your provider implementation. The User Storage SPI
has a callback whenever a user is cached: org.keycloak.models.cache.OnUserCache
.
public interface OnUserCache {
void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
}
Your provider class should implement this interface if it wants this callback. The UserModel
delegate parameter
is the UserModel
instance returned by your provider. The CachedUserModel
is an expanded UserModel
interface.
This is the instance that is cached locally in local storage.
public interface CachedUserModel extends UserModel {
/**
* Invalidates the cache for this user and returns a delegate that represents the actual data provider
*
* @return
*/
UserModel getDelegateForUpdate();
boolean isMarkedForEviction();
/**
* Invalidate the cache for this model
*
*/
void invalidate();
/**
* When was the model was loaded from database.
*
* @return
*/
long getCacheTimestamp();
/**
* Returns a map that contains custom things that are cached along with this model. You can write to this map.
*
* @return
*/
ConcurrentHashMap getCachedWith();
}
This CachedUserModel
interface allows you to evict the user from the cache and get the provider UserModel
instance.
The getCachedWith()
method returns a map that allows you to cache additional information pertaining to the user. For example, credentials are not part of the UserModel
interface. If you wanted to cache credentials in memory, you would implement OnUserCache
and cache your user’s credentials using the getCachedWith()
method.
Leveraging Jakarta EE
Since version 20, Keycloak relies only on Quarkus. Unlike WildFly, Quarkus is not an Application Server. For more detail, see https://www.keycloak.org/migration/migrating-to-quarkus#_quarkus_is_not_an_application_server.
Therefore, the User Storage Providers cannot be packaged within any Jakarta EE component or make it an EJB as was the case when Keycloak ran over WildFly in previous versions.
Providers implementations are required to be plain java objects which implement the suitable User Storage SPI interfaces, as was explained in the previous sections. And they must be packaged and deployed as stated in this Migration Guide:
You can still implement your custom UserStorageProvider
class, which is able to integrate an external database by JPA Entity Manager, as shown in this example:
CDI is not supported.
REST management API
You can create, remove, and update your user storage provider deployments through the administrator REST API. The User Storage SPI is built on top of a generic component interface so you will be using that generic API to manage your providers.
The REST Component API lives under your realm admin resource.
/admin/realms/{realm-name}/components
We will only show this REST API interaction with the Java client. Hopefully you can extract how to do this from curl
from this API.
public interface ComponentsResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query();
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query(@QueryParam("parent") String parent);
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query(@QueryParam("parent") String parent, @QueryParam("type") String type);
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ComponentRepresentation> query(@QueryParam("parent") String parent,
@QueryParam("type") String type,
@QueryParam("name") String name);
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response add(ComponentRepresentation rep);
@Path("{id}")
ComponentResource component(@PathParam("id") String id);
}
public interface ComponentResource {
@GET
public ComponentRepresentation toRepresentation();
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void update(ComponentRepresentation rep);
@DELETE
public void remove();
}
To create a user storage provider, you must specify the provider id, a provider type of the string org.keycloak.storage.UserStorageProvider
,
as well as the configuration.
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RealmRepresentation;
...
Keycloak keycloak = Keycloak.getInstance(
"http://localhost:8080",
"master",
"admin",
"password",
"admin-cli");
RealmResource realmResource = keycloak.realm("master");
RealmRepresentation realm = realmResource.toRepresentation();
ComponentRepresentation component = new ComponentRepresentation();
component.setName("home");
component.setProviderId("readonly-property-file");
component.setProviderType("org.keycloak.storage.UserStorageProvider");
component.setParentId(realm.getId());
component.setConfig(new MultivaluedHashMap());
component.getConfig().putSingle("path", "~/users.properties");
realmResource.components().add(component);
// retrieve a component
List<ComponentRepresentation> components = realmResource.components().query(realm.getId(),
"org.keycloak.storage.UserStorageProvider",
"home");
component = components.get(0);
// Update a component
component.getConfig().putSingle("path", "~/my-users.properties");
realmResource.components().component(component.getId()).update(component);
// Remove a component
realmREsource.components().component(component.getId()).remove();
Migrating from an earlier user federation SPI
This chapter is only applicable if you have implemented a provider using the earlier (and now removed) User Federation SPI. |
In Keycloak version 2.4.0 and earlier there was a User Federation SPI. Red Hat Single Sign-On version 7.0, although unsupported, had this earlier SPI available as well. This earlier User Federation SPI has been removed from Keycloak version 2.5.0 and Red Hat Single Sign-On version 7.1. However, if you have written a provider with this earlier SPI, this chapter discusses some strategies you can use to port it.
Import versus non-import
The earlier User Federation SPI required you to create a local copy of a user in the Keycloak’s database and import information from your external store to the local copy. However, this is no longer a requirement. You can still port your earlier provider as-is, but you should consider whether a non-import strategy might be a better approach.
Advantages of the import strategy:
-
Keycloak basically becomes a persistence user cache for your external store. Once the user is imported you’ll no longer hit the external store, thus taking load off of it.
-
If you are moving to Keycloak as your official user store and deprecating the earlier external store, you can slowly migrate applications to use Keycloak. When all applications have been migrated, unlink the imported user, and retire the earlier legacy external store.
There are some obvious disadvantages though to using an import strategy:
-
Looking up a user for the first time will require multiple updates to Keycloak database. This can be a big performance loss under load and put a lot of strain on the Keycloak database. The user federated storage approach will only store extra data as needed and might never be used depending on the capabilities of your external store.
-
With the import approach, you have to keep local Keycloak storage and external storage in sync. The User Storage SPI has capability interfaces that you can implement to support synchronization, but this can quickly become painful and messy.
UserFederationProvider versus UserStorageProvider
The first thing to notice is that UserFederationProvider
was a complete interface. You implemented every method in this interface. However, UserStorageProvider
has instead broken up this interface into multiple capability interfaces that you implement as needed.
UserFederationProvider.getUserByUsername()
and getUserByEmail()
have exact equivalents in the new SPI. The difference between the two is how you import. If you are going to continue with an import strategy, you no longer call KeycloakSession.userStorage().addUser()
to create the user locally. Instead you call KeycloakSession.userLocalStorage().addUser()
.
The userStorage()
method no longer exists.
The UserFederationProvider.validateAndProxy()
method has been moved to an optional capability interface, ImportedUserValidation
.
You want to implement this interface if you are porting your earlier provider as-is.
Also note that in the earlier SPI, this method was called every time the user was accessed, even if the local user is in the cache.
In the later SPI, this method is only called when the local user is loaded from local storage. If the local user is cached,
then the ImportedUserValidation.validate()
method is not called at all.
The UserFederationProvider.isValid()
method no longer exists in the later SPI.
The UserFederationProvider
methods synchronizeRegistrations()
, registerUser()
, and removeUser()
have been
moved to the UserRegistrationProvider
capability interface. This new interface is optional to implement so if your
provider does not support creating and removing users, you don’t have to implement it. If your earlier provider had switch
to toggle support for registering new users, this is supported in the new SPI, returning null
from
UserRegistrationProvider.addUser()
if the provider doesn’t support adding users.
The earlier UserFederationProvider
methods centered around credentials are now encapsulated in the CredentialInputValidator
and CredentialInputUpdater
interfaces, which are also optional to implement depending on if you support validating or
updating credentials. Credential management used to exist in UserModel
methods. These also have been moved to the
CredentialInputValidator
and CredentialInputUpdater
interfaces.
One thing to note that if you do not implement the CredentialInputUpdater
interface, then
any credentials provided by your provider can be overridden locally in Keycloak storage. So if you want
your credentials to be read-only, implement the CredentialInputUpdater.updateCredential()
method and
return a ReadOnlyException
.
The UserFederationProvider
query methods such as searchByAttributes()
and getGroupMembers()
are now encapsulated
in an optional interface UserQueryProvider
. If you do not implement this interface, then users will not be viewable
in the admin console. You’ll still be able to log in though.
UserFederationProviderFactory versus UserStorageProviderFactory
The synchronization methods in the earlier SPI are now encapsulated within an optional ImportSynchronization
interface.
If you have implemented synchronization logic, then have your new UserStorageProviderFactory
implement the
ImportSynchronization
interface.
Upgrading to a new model
The User Storage SPI instances are stored in a different set of relational tables. Keycloak
automatically runs a migration script. If any earlier User Federation providers are deployed for a realm, they are converted
to the later storage model as is, including the id
of the data. This migration will only happen if a User Storage provider exists
with the same provider ID (i.e., "ldap", "kerberos") as the earlier User Federation provider.
So, knowing this there are different approaches you can take.
-
You can remove the earlier provider in your earlier Keycloak deployment. This will remove the local linked copies of all users you imported. Then, when you upgrade Keycloak, just deploy and configure your new provider for your realm.
-
The second option is to write your new provider making sure it has the same provider ID:
UserStorageProviderFactory.getId()
. Make sure this provider is deployed to the server. Boot the server, and have the built-in migration script convert from the earlier data model to the later data model. In this case all your earlier linked imported users will work and be the same.
If you have decided to get rid of the import strategy and rewrite your User Storage provider, we suggest that you remove the earlier provider before upgrading Keycloak. This will remove linked local imported copies of any user you imported.
Stream-based interfaces
Many of the user storage interfaces in Keycloak contain query methods that can return potentially large sets of objects, which might lead to significant impacts in terms of memory consumption and processing time. This is especially true when only a small subset of the objects' internal state is used in the query method’s logic.
To provide developers with a more efficient alternative to process large data sets in these query methods, a Streams
sub-interface has been added to user storage interfaces. These Streams
sub-interfaces replace the original collection-based
methods in the super-interfaces with stream-based variants, making the collection-based methods default. The default implementation
of a collection-based query method invokes its Stream
counterpart and collects the result into the proper collection type.
The Streams
sub-interfaces allow for implementations to focus on the stream-based approach for processing sets of data and
benefit from the potential memory and performance optimizations of that approach. The interfaces that offer a Streams
sub-interface to be implemented include a few capability interfaces, all interfaces in the org.keycloak.storage.federated
package and a few others that might be implemented depending on the scope of the custom storage implementation.
See this list of the interfaces that offer a Streams
sub-interface to developers.
Package |
Classes |
|
|
|
|
|
|
|
All interfaces |
|
|
(*) indicates the interface is a capability interface
Custom user storage implementation that want to benefit from the streams approach should simply implement the Streams
sub-interfaces instead of the original interfaces. For example, the following code uses the Streams
variant of the UserQueryProvider
interface:
public class CustomQueryProvider extends UserQueryProvider.Streams {
...
@Override
Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults) {
// custom logic here
}
@Override
Stream<UserModel> searchForUserStream(String search, RealmModel realm) {
// custom logic here
}
...
}
Vault SPI
Vault provider
You can use a vault SPI from org.keycloak.vault
package to write custom extension for Keycloak to connect to arbitrary vault implementation.
The built-in files-plaintext
provider is an example of the implementation of this SPI. In general the following rules apply:
-
To prevent a secret from leaking across realms, you may want to isolate or limit the secrets that can be retrieved by a realm. In that case, your provider should take into account the realm name when looking up secrets, for example by prefixing entries with the realm name. For example, an expression
${vault.key}
would then evaluate generally to different entry names, depending on whether it was used in a realm A or realm B. To differentiate between realms, the realm needs to be passed to the createdVaultProvider
instance fromVaultProviderFactory.create()
method where it is available from theKeycloakSession
parameter. -
The vault provider needs to implement a single method
obtainSecret
that returns aVaultRawSecret
for the given secret name. That class holds the representation of the secret either inbyte[]
orByteBuffer
and is expected to convert between the two upon demand. Note that this buffer would be discarded after usage as explained below.
Regarding realm separation, all built-in vault provider factories allow the configuration of one or more key resolvers. Represented
by the VaultKeyResolver
interface, a key resolver essentially implements the algorithm or strategy for combining the realm name
with the key (as obtained from the ${vault.key}
expression) into the final entry name that will be used to retrieve the
secret from the vault. The code that handles this configuration has been extracted into abstract vault provider and vault
provider factory classes, so custom implementations that want to offer support for key resolvers may extend these abstract classes
instead of the implementing SPI interfaces to inherit the ability to configure the key resolvers that should be tried when retrieving a secret.
For details on how to package and deploy a custom provider refer to the Service Provider Interfaces chapter.
Consuming values from vault
The vault contains sensitive data and Keycloak treats the secrets accordingly. When accessing a secret, the secret is obtained from the vault and retained in JVM memory only for the necessary time. Then all possible attempts to discard its content from JVM memory is done. This is achieved by using the vault secrets only within try
-with-resources statement as outlined below:
char[] c;
try (VaultCharSecret cSecret = session.vault().getCharSecret(SECRET_NAME)) {
// ... use cSecret
c = cSecret.getAsArray().orElse(null);
// if c != null, it now contains password
}
// if c != null, it now contains garbage
The example uses KeycloakSession.vault()
as the entrypoint for accessing
the secrets. Using the VaultProvider.obtainSecret
method directly is indeed
also possible. However the vault()
method has the benefit of ability
to interpret the raw secret (which is generally a byte array)
as a character array (via vault().getCharSecret()
) or a String
(via vault().getStringSecret()
) in addition to obtaining the original
uninterpreted value (via vault().getRawSecret()
method).
Note that since String
objects are immutable, their content cannot be discarded
by overriding with random garbage. Even though measures have been taken in the default
VaultStringSecret
implementation to prevent internalizing String
s, the secrets
stored in String
objects would live at least to the next GC round. Thus using
plain byte and character arrays and buffers is preferable.