refactor
16
SUMMARY.adoc
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
== Keycloak Server Developer Guide
|
||||||
|
|
||||||
|
//. link:topics/templates/document-attributes.adoc[]
|
||||||
|
:imagesdir: images
|
||||||
|
|
||||||
|
. link:topics/preface.adoc[Preface]
|
||||||
|
. link:topics/providers.adoc[Providers and SPIs]
|
||||||
|
. link:topics/themes.adoc[Themes]
|
||||||
|
. link:topics/direct-access.adoc[Direct Access Grants]
|
||||||
|
. link:topics/admin-rest-api.adoc[Admin REST API]
|
||||||
|
. link:topics/events.adoc[Creating Event Listeners]
|
||||||
|
. link:topics/user-federation.adoc[User Federation SPI and LDAP/AD Integration]
|
||||||
|
. link:topics/custom-attributes.adoc[Custom User Attributes]
|
||||||
|
. link:topics/auth-spi.adoc[Custom Authentication, Registration, and Required Actions]
|
||||||
|
|
||||||
|
|
BIN
images/add-provider-dialog.png
Executable file
After Width: | Height: | Size: 88 KiB |
BIN
images/add-provider-select.png
Executable file
After Width: | Height: | Size: 59 KiB |
BIN
images/domain-mode.png
Executable file
After Width: | Height: | Size: 103 KiB |
BIN
images/email-simple-example.png
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
images/identity_broker_flow.png
Executable file
After Width: | Height: | Size: 21 KiB |
BIN
images/keycloak_logo.png
Executable file
After Width: | Height: | Size: 18 KiB |
BIN
images/update-server-config-dialog.png
Executable file
After Width: | Height: | Size: 66 KiB |
BIN
images/update-server-config-select.png
Executable file
After Width: | Height: | Size: 80 KiB |
12
topics/admin-rest-api.adoc
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
= Admin REST API
|
||||||
|
|
||||||
|
The Keycloak Admin Console is implemented entirely with a fully functional REST admin API.
|
||||||
|
You can invoke this REST API from your Java applications by obtaining an access token.
|
||||||
|
|
||||||
|
You must have the appropriate permissions set up as described in <<_admin_permissions>> and <<_per_realm_admin_permissions>>
|
||||||
|
|
||||||
|
The documentation for this REST API is auto-generated and is contained in the distribution of keycloak under the docs/rest-api/overview-index.html directory, or directly from the docs page at the keycloak website.
|
||||||
|
|
||||||
|
There are a number of examples that come with the keycloak distribution that show you how to invoke on this REST API. `examples/preconfigured-demo/admin-access-app` shows you how to access this api from java. `examples/cors/angular-product-app` shows you how to invoke on it from Javascript.
|
||||||
|
|
||||||
|
Finally there is example in `example/admin-client`, which contains example for Admin client, that can be used to invoke REST endpoints easily as Java methods.
|
794
topics/auth-spi.adoc
Executable file
|
@ -0,0 +1,794 @@
|
||||||
|
[[_auth_spi]]
|
||||||
|
= Custom Authentication, Registration, and Required Actions
|
||||||
|
|
||||||
|
Keycloak comes out of the box with a bunch of different authentication mechanisms: kerberos, password, and otp.
|
||||||
|
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 i.e.
|
||||||
|
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-grain 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.
|
||||||
|
|
||||||
|
== 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, optional, required, or an alternative.
|
||||||
|
An alternative requirement means that the authentiactor is optional unless no other alternative authenticator is successful in the flow.
|
||||||
|
For example, cookie authentication, kerberos, and the set of all login forms are all alternative.
|
||||||
|
If one of those is successful, none of the others are executed.
|
||||||
|
|
||||||
|
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.
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
Cookie - ALTERNATIVE
|
||||||
|
Kerberos - ALTERNATIVE
|
||||||
|
Forms Subflow - ALTERNATIVE
|
||||||
|
Username/Password Form - REQUIRED
|
||||||
|
OTP Password Form - OPTIONAL
|
||||||
|
----
|
||||||
|
|
||||||
|
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 relevent data, verifies the client and any signatures.
|
||||||
|
It creates a ClientSessionModel.
|
||||||
|
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 client 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 ClientSessionModel.
|
||||||
|
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 ClientSessionModel 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 ClientSession 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 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 ClientSessionModel and returns a status of success()
|
||||||
|
. The next execution is the OTP Form.
|
||||||
|
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.
|
||||||
|
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, and this execution is required, 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 execution was optional, then this execution is skipped.
|
||||||
|
. After the flow is complete, the authentication processor creates a UserSessionModel and associates it with the ClientSessionModel.
|
||||||
|
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 require actions list.
|
||||||
|
. After all required actions have been resolved, the user is finally logged in.
|
||||||
|
|
||||||
|
[[_auth_spi_walkthrough]]
|
||||||
|
== 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 examples/providers/authenticator directory of the demo distribution of Keycloak.
|
||||||
|
|
||||||
|
The classes you must implement are 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.
|
||||||
|
|
||||||
|
=== 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 classname of each AuthenticatorFactory implementation you have in the jar.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
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 standalone/configuration/providers directory.
|
||||||
|
|
||||||
|
=== Implementing an Authenticator
|
||||||
|
|
||||||
|
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.
|
||||||
|
For this example, we need to check if the answer to the secret question has been set up by the user or not.
|
||||||
|
In our case we are storing this information, hashed, within a UserCredentialValueModel within the UserModel (just like passwords are stored). Here's how we do this very simple check:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
@Override
|
||||||
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
return session.users().configuredForCredentialType("secret_question", realm, user);
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
The configuredForCredentialType() call queries the user to see if it supports that credential type.
|
||||||
|
|
||||||
|
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.
|
||||||
|
It 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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
@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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
if (hasCookie(context)) {
|
||||||
|
context.success();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Response challenge = loginForm(context).createForm("secret_question.ftl");
|
||||||
|
context.challenge(challenge);
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
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 enteres 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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@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 last thing I want to go over is the setCookie() method.
|
||||||
|
This is an example of providing configuration for the Authenticator.
|
||||||
|
In this case we want the max age of the cookie to be configurable.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
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"));
|
||||||
|
|
||||||
|
}
|
||||||
|
... set the cookie ...
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
=== 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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
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 specify the allowed requirement switches.
|
||||||
|
While there are four different requirement types: ALTERNATIVE, REQUIRED, OPTIONAL, DISABLED, AuthenticatorFactory implementations can limit which requirement options are shown in the admin console when defining a flow.
|
||||||
|
In our example, we're going to limit our requirement options to REQUIRED and DISABLED.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||||
|
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||||
|
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().
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@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 what text that will be shown in the admin console when listing the Authenticator.
|
||||||
|
getReferenceCategory() is just a category the Authenticator belongs to.
|
||||||
|
|
||||||
|
=== Adding Authenticator Form
|
||||||
|
|
||||||
|
Keycloak comes with a Freemarker <<_themes,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 placed in the login theme with all the other .ftl files you see for login.
|
||||||
|
|
||||||
|
Let's take a bigger look at secret-question.ftl Here's a small code snippet:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
<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>
|
||||||
|
----
|
||||||
|
|
||||||
|
Any piece of text enclosed in `${}` corresponds to an attribute or template funtion.
|
||||||
|
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` value.
|
||||||
|
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.
|
||||||
|
|
||||||
|
[[_adding_authenticator]]
|
||||||
|
=== Adding 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 an 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 Authenticator.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
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, just copy it to the standalone/configuration/providers directory.
|
||||||
|
|
||||||
|
=== 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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@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 actin provider instance.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
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/Extending the Registration Form
|
||||||
|
|
||||||
|
It is entirely possible for you to implement your own flow with a set of Authenticators to totally change how regisration is done in Keycloak.
|
||||||
|
But what you'll usually want to do is just add a little 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 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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void buildPage(FormContext context, LoginFormsProvider form) {
|
||||||
|
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
|
||||||
|
if (captchaConfig == null || captchaConfig.getConfig() == null
|
||||||
|
|| captchaConfig.getConfig().get(SITE_KEY) == null
|
||||||
|
|| captchaConfig.getConfig().get(SITE_SECRET) == null
|
||||||
|
) {
|
||||||
|
form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String siteKey = captchaConfig.getConfig().get(SITE_KEY);
|
||||||
|
form.setAttribute("recaptchaRequired", true);
|
||||||
|
form.setAttribute("recaptchaSiteKey", siteKey);
|
||||||
|
form.addScript("https://www.google.com/recaptcha/api.js");
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
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 was as Authenticators are.
|
||||||
|
In this example, we pull the Google Recaptcha site key from configuration and add it as an attribute to the form provider.
|
||||||
|
Our regstration template file can read this attribute now.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(ValidationContext context) {
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
List<FormMessage> errors = new ArrayList<>();
|
||||||
|
boolean success = false;
|
||||||
|
|
||||||
|
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
|
||||||
|
if (!Validation.isBlank(captcha)) {
|
||||||
|
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
|
||||||
|
String secret = captchaConfig.getConfig().get(SITE_SECRET);
|
||||||
|
|
||||||
|
success = validateRecaptcha(context, success, captcha, secret);
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
context.success();
|
||||||
|
} else {
|
||||||
|
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
|
||||||
|
formData.remove(G_RECAPTCHA_RESPONSE);
|
||||||
|
context.validationError(formData, errors);
|
||||||
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
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.
|
||||||
|
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, i.e.
|
||||||
|
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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(ValidationContext context) {
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
List<FormMessage> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
String eventError = Errors.INVALID_REGISTRATION;
|
||||||
|
|
||||||
|
if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_FIRST_NAME)))) {
|
||||||
|
errors.add(new FormMessage(RegistrationPage.FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_LAST_NAME)))) {
|
||||||
|
errors.add(new FormMessage(RegistrationPage.FIELD_LAST_NAME, Messages.MISSING_LAST_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
String email = formData.getFirst(Validation.FIELD_EMAIL);
|
||||||
|
if (Validation.isBlank(email)) {
|
||||||
|
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
|
||||||
|
} else if (!Validation.isEmailValid(email)) {
|
||||||
|
formData.remove(Validation.FIELD_EMAIL);
|
||||||
|
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
|
||||||
|
formData.remove(Validation.FIELD_EMAIL);
|
||||||
|
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.size() > 0) {
|
||||||
|
context.validationError(formData, errors);
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
context.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
As you can see, this validate() method of user profile processing makes sure that the email, first, and last name are filled in in the form.
|
||||||
|
It also makes sure that email is in the right format.
|
||||||
|
If any of these validations fail, an error message is queued up for rendering.
|
||||||
|
Any fields in error are removed from the form data.
|
||||||
|
Error messages are represented by the FormMessage class.
|
||||||
|
The first parameter of the constructor of this class takes the HTML element id.
|
||||||
|
The input in error will be highlighted when the form is re-rendered.
|
||||||
|
The second parameter is a message reference id.
|
||||||
|
This id must correspond to a property in one of the localized message bundle files.
|
||||||
|
in the theme.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void success(FormContext context) {
|
||||||
|
UserModel user = context.getUser();
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
user.setFirstName(formData.getFirst(RegistrationPage.FIELD_FIRST_NAME));
|
||||||
|
user.setLastName(formData.getFirst(RegistrationPage.FIELD_LAST_NAME));
|
||||||
|
user.setEmail(formData.getFirst(RegistrationPage.FIELD_EMAIL));
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Pretty simple implementation.
|
||||||
|
The UserModel of the newly registered user is obtained from the FormContext.
|
||||||
|
The appropriate methods are called to initialize UserModel data.
|
||||||
|
|
||||||
|
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 classname of each FormActionFactory implementation you have in the jar.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
|
||||||
|
org.keycloak.authentication.forms.RegistrationProfile
|
||||||
|
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, just copy it to the standalone/configuration/providers directory.
|
||||||
|
|
||||||
|
=== Adding FormAction to the Registration Flow
|
||||||
|
|
||||||
|
Adding an 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 an 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 errors 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 Regsitration 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 in 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.
|
||||||
|
More details about this flow are in the <<_identity_broker_first_login,Identity provider chapter>>.
|
||||||
|
|
||||||
|
[[_client_authentication]]
|
||||||
|
== Authentication of clients
|
||||||
|
|
||||||
|
Keycloak actually supports pluggable authentication for http://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect] client applications.
|
||||||
|
Authentication of client (application) is used under the hood by the <<_adapter_config,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,Direct Access grants>> or during <<_service_accounts,Service account>> authentication.
|
||||||
|
|
||||||
|
=== Default implementations
|
||||||
|
|
||||||
|
Actually Keycloak has 2 builtin implementations of client authentication:
|
||||||
|
|
||||||
|
Traditional authentication with client_id and client_secret::
|
||||||
|
This is default mechanism mentioned in the http://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect] or http://tools.ietf.org/html/rfc6749[OAuth2] specification and Keycloak supports it since it's early days.
|
||||||
|
The public client needs to include `client_id` parameter with it's ID in the POST request (so it's defacto not authenticated) and the confidential client needs to include `Authorization: Basic` header with the clientId and clientSecret used as username and password.
|
||||||
|
|
||||||
|
Authentication with signed JWT::
|
||||||
|
This is based on the https://tools.ietf.org/html/rfc7523[JWT Bearer Token Profiles for OAuth 2.0] specification.
|
||||||
|
The client/adapter generates the https://tools.ietf.org/html/rfc7519[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/service-account` for the example application showing service accounts authentication with both clientId+clientSecret and 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` and `org.keycloak.authentication.ClientAuthenticator` . You also need to add the file `META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory` with the name of the implementation classes.
|
||||||
|
See <<_auth_spi_walkthrough,authenticators>> for more details.
|
115
topics/custom-attributes.adoc
Executable file
|
@ -0,0 +1,115 @@
|
||||||
|
= Custom User Attributes
|
||||||
|
|
||||||
|
If you have custom user data you want to store and manage in the admin console, registration page, and user account service, you can easily add support for it by extending and modifying various Keycloak <<_themes,themes>>.
|
||||||
|
|
||||||
|
== In admin console
|
||||||
|
|
||||||
|
To be able to enter custom attributes in the admin console, take the following steps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
. themes/mytheme/admin
|
||||||
|
+`mytheme`
|
||||||
|
. theme.properties
|
||||||
|
+
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
parent=keycloak
|
||||||
|
import=common/keycloak
|
||||||
|
----
|
||||||
|
|
||||||
|
. themes/base/admin/resources/partials/user-attributes.html
|
||||||
|
+`themes/mytheme/admin/resources/partials/user-attributes.html`
|
||||||
|
. user-attributes.html
|
||||||
|
+
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
<div class="form-group clearfix block">
|
||||||
|
<label class="col-sm-2 control-label" for="mobile">Mobile</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input ng-model="user.attributes.mobile" class="form-control" type="text" name="mobile" id="mobile" />
|
||||||
|
</div>
|
||||||
|
<span tooltip-placement="right" tooltip="Mobile number." class="fa fa-info-circle"></span>
|
||||||
|
</div>
|
||||||
|
----
|
||||||
|
+`ng-model`
|
||||||
|
+`user.attributes.ATTR_NAME`
|
||||||
|
. Change the theme for the admin console. Save it, then refresh your browser, and you should
|
||||||
|
now see these fields in the User detail page for any user.
|
||||||
|
|
||||||
|
== In registration page
|
||||||
|
|
||||||
|
To be able to enter custom attributes in the registration page, take the following steps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
. themes/mytheme/login
|
||||||
|
+`mytheme`
|
||||||
|
. theme.properties
|
||||||
|
+
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
parent=keycloak
|
||||||
|
import=common/keycloak
|
||||||
|
styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/login.css ../patternfly/lib/zocial/zocial.css css/login.css
|
||||||
|
----
|
||||||
|
|
||||||
|
. themes/base/login/register.ftl
|
||||||
|
+`themes/mytheme/login/register.ftl`
|
||||||
|
. register.ftl
|
||||||
|
+
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="${properties.kcLabelWrapperClass!}">
|
||||||
|
<label for="user.attributes.mobile" class="${properties.kcLabelClass!}">Mobile number</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-10 col-md-10">
|
||||||
|
<input type="text" class="${properties.kcInputClass!}" id="user.attributes.mobile" name="user.attributes.mobile"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
----
|
||||||
|
+`user.attributes.ATTR_NAME`
|
||||||
|
. Change the theme for the login to your new theme. Save it, then refresh your browser, and you should
|
||||||
|
now see these fields in the registration.
|
||||||
|
|
||||||
|
== In user account profile page
|
||||||
|
|
||||||
|
To be able to manage custom attributes in the user account profile page, take the following steps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
. themes/mytheme/account
|
||||||
|
+`mytheme`
|
||||||
|
. theme.properties
|
||||||
|
+
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
parent=patternfly
|
||||||
|
import=common/keycloak
|
||||||
|
|
||||||
|
styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/account.css css/account.css
|
||||||
|
----
|
||||||
|
|
||||||
|
. themes/base/account/account.ftl
|
||||||
|
+`themes/mytheme/account/account.ftl`
|
||||||
|
. account.ftl
|
||||||
|
+
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-2 col-md-2">
|
||||||
|
<label for="user.attributes.mobile" class="control-label">Mobile number</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-10 col-md-10">
|
||||||
|
<input type="text" class="form-control" id="user.attributes.mobile" name="user.attributes.mobile" value="${(account.attributes.mobile!'')?html}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
----
|
||||||
|
+`user.attributes.ATTR_NAME`
|
||||||
|
. Change the theme for the account to your new theme. Save it, then refresh your browser, and you should
|
||||||
|
now see these fields in the account profile page.
|
159
topics/direct-access.adoc
Executable file
|
@ -0,0 +1,159 @@
|
||||||
|
= Direct Access Grants
|
||||||
|
|
||||||
|
Keycloak allows you to make direct REST invocations to obtain an access token.
|
||||||
|
(See http://tools.ietf.org/html/rfc6749#section-4.3[Resource Owner Password Credentials Grant] from OAuth 2.0 spec). To use it you must also have registered a valid Client to use as the "client_id" for this grant request.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
It is highly recommended that you do not use Direct Access Grants to write your own login pages for your application.
|
||||||
|
You will lose a lot of features that Keycloak has if you do this.
|
||||||
|
Specifically all the account management, remember me, lost password, account reset features of Keycloak.
|
||||||
|
Instead, if you want to tailor the look and feel of Keycloak login pages, you should create your own <<_themes,theme>>.
|
||||||
|
There are also security implications to using Direct Access Grants compared to the redirect based flows as you are exposing plain text passwords to applications directly.
|
||||||
|
|
||||||
|
It is even highly recommended that you use the browser to log in for native mobile applications! Android and iPhone applications allow you to redirect to and from the browser.
|
||||||
|
You can use this to redirect the user from your native mobile app to the web browser to perform login, then the browser will redirect back to your native application.
|
||||||
|
====
|
||||||
|
|
||||||
|
The REST URL to invoke on is `/{keycloak-root}/realms/{realm-name}/protocol/openid-connect/token`.
|
||||||
|
Invoking on this URL is a POST request and requires you to post the username and credentials of the user you want an access token for.
|
||||||
|
You must also pass along the "client_id" of the client you are creating an access token for.
|
||||||
|
This "client_id" is the Client Id specified in admin console (not it's id from DB!). Depending on whether your client is <<_access_types,"public" or "confidential">>, you may also have to pass along it's client secret as well.
|
||||||
|
We support pluggable client authentication, so alternatively you can use other form of client credentials like signed JWT assertion.
|
||||||
|
See <<_client_authentication,Client Authentication>> section for more details.
|
||||||
|
Finally you need to pass "grant_type" parameter with value "password" .
|
||||||
|
|
||||||
|
For public client's, the POST invocation requires form parameters that contain the username, credentials, and client_id of your application.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
POST /auth/realms/demo/protocol/openid-connect/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
username=bburke&password=geheim&client_id=customer-portal&grant_type=password
|
||||||
|
----
|
||||||
|
The response would be this http://tools.ietf.org/html/rfc6749#section-4.3.3[standard JSON document] from the OAuth 2.0 specification.
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
Cache-Control: no-store
|
||||||
|
Pragma: no-cache
|
||||||
|
|
||||||
|
{
|
||||||
|
"access_token":"2YotnFZFEjr1zCsicMWpAA",
|
||||||
|
"token_type":"bearer",
|
||||||
|
"expires_in":3600,
|
||||||
|
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
|
||||||
|
"id_token":"tGzv3JOkF0XG5Qx2TlKWIA",
|
||||||
|
"session_state":"234234-234234-234234"
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
For confidential client's, you must create a Basic Auth `Authorization` header that contains the client_id and client secret.
|
||||||
|
And pass in the form parameters for username and for each user credential.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
POST /auth/realms/demo/protocol/openid-connect/token
|
||||||
|
Authorization: Basic atasdf023l2312023
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
username=bburke&password=geheim&grant_type=password
|
||||||
|
----
|
||||||
|
|
||||||
|
As mentioned above, we support also other means of authenticating clients.
|
||||||
|
In adition to default client_id and client secret, we also have signed JWT assertion by default.
|
||||||
|
There is possibility to use any other form of client authentication implemented by you.
|
||||||
|
See <<_client_authentication,Client Authentication>> section for more details.
|
||||||
|
|
||||||
|
Here's a Java example using Apache HTTP Client and some Keycloak utility classes.:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
HttpClient client = new HttpClientBuilder()
|
||||||
|
.disableTrustManager().build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpPost post = new HttpPost(
|
||||||
|
KeycloakUriBuilder.fromUri("http://localhost:8080/auth")
|
||||||
|
.path(ServiceUrlConstants.TOKEN_PATH).build("demo"));
|
||||||
|
List <NameValuePair> formparams = new ArrayList <NameValuePair>();
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "password"));
|
||||||
|
formparams.add(new BasicNameValuePair("username", "bburke"));
|
||||||
|
formparams.add(new BasicNameValuePair("password", "password"));
|
||||||
|
|
||||||
|
if (isPublic()) { // if client is public access type
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
|
||||||
|
} else {
|
||||||
|
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret");
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
|
||||||
|
HttpResponse response = client.execute(post);
|
||||||
|
int status = response.getStatusLine().getStatusCode();
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 200) {
|
||||||
|
throw new IOException("Bad status: " + status);
|
||||||
|
}
|
||||||
|
if (entity == null) {
|
||||||
|
throw new IOException("No Entity");
|
||||||
|
}
|
||||||
|
InputStream is = entity.getContent();
|
||||||
|
try {
|
||||||
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
is.close();
|
||||||
|
} catch (IOException ignored) { }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.getConnectionManager().shutdown();
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Once you have the access token string, you can use it in REST HTTP bearer token authorized requests, i.e
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
GET /my/rest/api
|
||||||
|
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
|
||||||
|
----
|
||||||
|
|
||||||
|
To logout you must use the refresh token contained in the AccessTokenResponse object.
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
|
if (isPublic()) { // if client is public access type
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
|
||||||
|
} else {
|
||||||
|
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret");
|
||||||
|
post.setHeader("Authorization", authorization);
|
||||||
|
}
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, tokenResponse.getRefreshToken()));
|
||||||
|
HttpResponse response = null;
|
||||||
|
URI logoutUri = KeycloakUriBuilder.fromUri(getBaseUrl(request) + "/auth")
|
||||||
|
.path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||||
|
.build("demo");
|
||||||
|
HttpPost post = new HttpPost(logoutUri);
|
||||||
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
response = client.execute(post);
|
||||||
|
int status = response.getStatusLine().getStatusCode();
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 204) {
|
||||||
|
error(status, entity);
|
||||||
|
}
|
||||||
|
if (entity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InputStream is = entity.getContent();
|
||||||
|
if (is != null) is.close();
|
||||||
|
----
|
5
topics/events.adoc
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
= Creating Event LIsteners
|
||||||
|
|
||||||
|
Keycloak provides an Events SPI that makes it possible to register listeners for user related events, for example user logins.
|
||||||
|
There are two interfaces that can be implemented, the first is a pure listener, the second is a events store which listens for events, but is also required to store events.
|
||||||
|
An events store provides a way for the admin and account management consoles to view events.
|
20
topics/preface.adoc
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
= 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:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
Let's pretend to have an extremely \
|
||||||
|
long line that \
|
||||||
|
does not fit
|
||||||
|
This one is short
|
||||||
|
----
|
||||||
|
Is really:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
Let's pretend to have an extremely long line that does not fit
|
||||||
|
This one is short
|
||||||
|
----
|
||||||
|
|
281
topics/providers.adoc
Executable file
|
@ -0,0 +1,281 @@
|
||||||
|
[[_providers]]
|
||||||
|
|
||||||
|
= Providers and SPIs
|
||||||
|
|
||||||
|
Keycloak is designed to cover most use-cases without requiring custom code, but we also want it to be customizable.
|
||||||
|
To achive this Keycloak has a number of SPIs which you can implement your own providers for.
|
||||||
|
|
||||||
|
== Implementing a SPI
|
||||||
|
|
||||||
|
To implement an SPI you need to implement it's ProviderFactory and Provider interfaces.
|
||||||
|
You also need to create a provider-configuration file.
|
||||||
|
For example to implement the Event Listener SPI you need to implement EventListenerProviderFactory and EventListenerProvider and also provide the file `META-INF/services/org.keycloak.events.EventListenerProviderFactory`
|
||||||
|
|
||||||
|
For example to implement the Event Listener SPI you start by implementing EventListenerProviderFactory:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
package org.acme.provider;
|
||||||
|
|
||||||
|
import ...
|
||||||
|
|
||||||
|
public class MyEventListenerProviderFactory implements EventListenerProviderFactory {
|
||||||
|
|
||||||
|
private List<Event> events;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return "my-event-listener";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
int max = config.getInt("max");
|
||||||
|
events = new MaxList(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventListenerProvider create(KeycloakSession session) {
|
||||||
|
return new MyEventListenerProvider(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
events = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
The example uses an imagined MaxList which has a maximum size and is concurrency safe.
|
||||||
|
When the maximum size is reached and new entries are added the oldest entry is removed.
|
||||||
|
Keycloak creates a single instance of `EventListenerProviderFactory` which makes it possible to store state for multiple requests.
|
||||||
|
`EventListenerProvider` instances are created by calling create on the factory for each requests so these should be light-weight.
|
||||||
|
|
||||||
|
Next you would implement `EventListenerProvider`:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
package org.acme.provider;
|
||||||
|
|
||||||
|
import ...
|
||||||
|
|
||||||
|
public class MyEventListenerProvider implements EventListenerProvider {
|
||||||
|
|
||||||
|
private List<Event> events;
|
||||||
|
|
||||||
|
public MyEventListenerProvider(List<Event> events) {
|
||||||
|
this.events = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(Event event) {
|
||||||
|
events.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
The file `META-INF/services/org.keycloak.events.EventListenerProviderFactory` should contain the full name of your ProviderFactory implementation:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
org.acme.provider.MyEventListenerProviderFactory
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Show info from you SPI implementation in Keycloak admin console
|
||||||
|
|
||||||
|
Sometimes it is useful to show additional info about your Provider to a Keycloak administrator.
|
||||||
|
You can show provider build time informations (eg. version of custom provider currently installed), current configuration of the provider (eg. 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 `MyEventListenerProviderFactory` from previous example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
package org.acme.provider;
|
||||||
|
|
||||||
|
import ...
|
||||||
|
|
||||||
|
public class MyEventListenerProviderFactory implements EventListenerProviderFactory, ServerInfoAwareProviderFactory {
|
||||||
|
|
||||||
|
private List<Event> events;
|
||||||
|
private int max;
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
max = config.getInt("max");
|
||||||
|
events = new MaxList(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getOperationalInfo() {
|
||||||
|
Map<String, String> ret = new LinkedHashMap<>();
|
||||||
|
ret.put("version", "1.0");
|
||||||
|
ret.put("listSizeMax", max + "");
|
||||||
|
ret.put("listSizeCurrent", events.size() + "");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
== Registering provider implementations
|
||||||
|
|
||||||
|
Keycloak can load provider implementations from JBoss Modules or directly from the file-system.
|
||||||
|
Using Modules is recommended as you can control exactly what classes are available to your provider.
|
||||||
|
Any providers loaded from the file-system uses a classloader with the Keycloak classloader as its parent.
|
||||||
|
|
||||||
|
=== Register a provider using Modules
|
||||||
|
|
||||||
|
To register a provider using Modules first create a module.
|
||||||
|
To do this you can either use the jboss-cli script or manually create a folder inside `KEYCLOAK_HOME/modules` and add your jar and a `module.xml`.
|
||||||
|
For example to add the event listener sysout example provider using the `jboss-cli` script execute:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.event-sysout --resources=target/event-listener-sysout-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-events-api"
|
||||||
|
----
|
||||||
|
Or to manually create it start by creating the folder `KEYCLOAK_HOME/modules/org/keycloak/examples/event-sysout/main`.
|
||||||
|
Then copy `event-listener-sysout-example.jar` to this folder and create `module.xml` with the following content:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.examples.event-sysout">
|
||||||
|
<resources>
|
||||||
|
<resource-root path="event-listener-sysout-example.jar"/>
|
||||||
|
</resources>
|
||||||
|
<dependencies>
|
||||||
|
<module name="org.keycloak.keycloak-core"/>
|
||||||
|
<module name="org.keycloak.keycloak-server-spi"/>
|
||||||
|
</dependencies>
|
||||||
|
</module>
|
||||||
|
----
|
||||||
|
|
||||||
|
Once you've created the module you need to register this module with Keycloak.
|
||||||
|
This is done by editing keycloak-server.json and adding it to the providers:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
...
|
||||||
|
"module:org.keycloak.examples.event-sysout"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Register a provider using file-system
|
||||||
|
|
||||||
|
To register your provider simply copy the JAR including the ProviderFactory and Provider classes and the provider configuration file to server's root `providers` directory.
|
||||||
|
|
||||||
|
You can also define multiple provider class-path if you want to create isolated class-loaders.
|
||||||
|
To do this edit keycloak-server.json and add more classpath entries to the providers array.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
"classpath:provider1.jar;lib-v1.jar",
|
||||||
|
"classpath:provider2.jar;lib-v2.jar"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
The above example will create two separate class-loaders for providers.
|
||||||
|
The classpath entries follow the same syntax as Java classpath, with ';' separating multiple-entries.
|
||||||
|
Wildcard is also supported allowing loading all jars (files with .jar or .JAR extension) in a folder, for example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
"classpath:/home/user/providers/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Configuring a provider
|
||||||
|
|
||||||
|
You can pass configuration options to your provider by setting them in `keycloak-server.json`.
|
||||||
|
For example to set the max value for `my-event-listener` add:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
{
|
||||||
|
"eventsListener": {
|
||||||
|
"my-event-listener": {
|
||||||
|
"max": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Disabling a provider
|
||||||
|
|
||||||
|
You can disable a provider by setting the enabled field for the provider to false in `keycloak-server.json`.
|
||||||
|
For example to disable the Infinispan user cache provider add:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
{
|
||||||
|
"userCache": {
|
||||||
|
"infinispan" : {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
== Available SPIs
|
||||||
|
|
||||||
|
Here's a list of the available SPIs and a brief description.
|
||||||
|
For more details on each SPI refer to individual sections.
|
||||||
|
|
||||||
|
Account::
|
||||||
|
|
||||||
|
Connections Infinispan::
|
||||||
|
|
||||||
|
Connections Jpa::
|
||||||
|
|
||||||
|
Connections Jpa Updater::
|
||||||
|
|
||||||
|
Connections Mongo::
|
||||||
|
|
||||||
|
Email::
|
||||||
|
|
||||||
|
Events Listener::
|
||||||
|
|
||||||
|
Events Store::
|
||||||
|
|
||||||
|
Export::
|
||||||
|
|
||||||
|
Import::
|
||||||
|
|
||||||
|
Login::
|
||||||
|
|
||||||
|
Login Protocol::
|
||||||
|
|
||||||
|
Realm::
|
||||||
|
|
||||||
|
Realm Cache::
|
||||||
|
|
||||||
|
Theme::
|
||||||
|
|
||||||
|
Timer::
|
||||||
|
|
||||||
|
User::
|
||||||
|
|
||||||
|
User Cache::
|
||||||
|
|
||||||
|
User Federation::
|
||||||
|
|
||||||
|
User Sessions::
|
292
topics/themes.adoc
Executable file
|
@ -0,0 +1,292 @@
|
||||||
|
= 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 support several types to customize different aspects of Keycloak.
|
||||||
|
The types currently available are:
|
||||||
|
|
||||||
|
* Account - Account management
|
||||||
|
* Admin - Admin console
|
||||||
|
* Email - Emails
|
||||||
|
* Login - Login forms
|
||||||
|
* Welcome - Welcome pages
|
||||||
|
|
||||||
|
== Configure theme
|
||||||
|
|
||||||
|
All theme types, except welcome, is configured through `Keycloak Admin Console`.
|
||||||
|
To change the theme used for a realm open the `Keycloak Admin Console`, select your realm from the drop-down box in the top left corner.
|
||||||
|
Under `Settings` click on `Theme`.
|
||||||
|
|
||||||
|
To set the theme for the `master` Keycloak admin console set the admin console theme for the `master` realm.
|
||||||
|
To set the theme for per realm admin access control set the admin console theme for the corresponding realm.
|
||||||
|
|
||||||
|
To change the welcome theme you need to edit `standalone/configuration/keycloak-server.json` and add `welcomeTheme` to the theme element, for example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
"theme": {
|
||||||
|
...
|
||||||
|
"welcomeTheme": "custom-theme"
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
== Default themes
|
||||||
|
|
||||||
|
Keycloak comes bundled with default themes in the server's root `themes` directory.
|
||||||
|
You should not edit the bundled themes directly.
|
||||||
|
Instead create a new theme that extends a bundled theme.
|
||||||
|
|
||||||
|
== Creating a theme
|
||||||
|
|
||||||
|
A theme consists of:
|
||||||
|
|
||||||
|
* FreeMarker
|
||||||
|
* Stylesheets
|
||||||
|
* Scripts
|
||||||
|
* Images
|
||||||
|
* Message bundles
|
||||||
|
* Theme properties
|
||||||
|
|
||||||
|
A theme can extend another theme.
|
||||||
|
When extending a theme you can override individual files (templates, stylesheets, etc.). The recommended way to create a theme is to extend the base theme.
|
||||||
|
The base theme provides templates and a default message bundle.
|
||||||
|
If you decide to override templates bear in mind that you may need to update your templates when upgrading to a new release to include any changes made to the original template.
|
||||||
|
|
||||||
|
Before creating a theme it's a good idea to disable caching as this makes it possible to edit theme resources without restarting the server.
|
||||||
|
To do this open `../standalone/configuration/keycloak-server.json` for `theme` set `staticMaxAge` to `-1` and `cacheTemplates` and `cacheThemes` to `false`.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
[
|
||||||
|
"theme": {
|
||||||
|
"default": "keycloak",
|
||||||
|
"staticMaxAge": -1,
|
||||||
|
"cacheTemplates": false,
|
||||||
|
"cacheThemes": false,
|
||||||
|
"folder": {
|
||||||
|
"dir": "${jboss.home.dir}/themes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
----
|
||||||
|
Remember to re-enable caching in production as it will significantly impact performance.
|
||||||
|
|
||||||
|
To create a new theme create a directory for the theme in the server's root `themes`.
|
||||||
|
The name of the directory should be the name of the theme.
|
||||||
|
For example to create a theme called `example-theme` create the directory `themes/example-theme`.
|
||||||
|
Inside the theme directory you then need to create a directory for each of the types your theme is going to provide.
|
||||||
|
For example to add the login type to the `example-theme` theme create the directory `themes/example-theme/login`.
|
||||||
|
|
||||||
|
For each type create a file `theme.properties` which allows setting some configuration for the theme, for example what theme it overrides and if it should import any themes.
|
||||||
|
For the above example we want to override the base theme and import common resources from the Keycloak theme.
|
||||||
|
To do this create the file `themes/example-theme/login/theme.properties` with following contents:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
[
|
||||||
|
parent=base
|
||||||
|
import=common/keycloak
|
||||||
|
----
|
||||||
|
|
||||||
|
You have now created a theme with support for the login type.
|
||||||
|
To check that it works open the admin console.
|
||||||
|
Select your realm and click on `Themes`.
|
||||||
|
For `Login Theme` select `example-theme` and click `Save`.
|
||||||
|
Then open the login page for the realm.
|
||||||
|
You can do this either by login through your application or by opening `http://localhost:8080/realms/<realm name>/account`.
|
||||||
|
|
||||||
|
To see the effect of changing the parent theme, set `parent=keycloak` in `theme.properties` and refresh the login page.
|
||||||
|
To follow the rest of the documentation set it back to `parent=base` before continuing.
|
||||||
|
|
||||||
|
=== Stylesheets
|
||||||
|
|
||||||
|
A theme can have one or more stylesheets, to add a stylesheet create a file inside `resources/css` (for example `resources/css/styles.css`) inside your theme folder.
|
||||||
|
Then registering it in `theme.properties` by adding:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
styles=css/styles.css
|
||||||
|
----
|
||||||
|
|
||||||
|
The `styles` property supports a space separated list so you can add as many as you want.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
styles=css/styles.css css/more-styles.css
|
||||||
|
----
|
||||||
|
`example-theme/login/resources/css/styles.css`
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
#kc-form {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
----
|
||||||
|
`example-theme/login/theme.properties`
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
styles=css/styles.css
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Scripts
|
||||||
|
|
||||||
|
A theme can have one or more scripts, to add a script create a file inside `resources/js` (for example `resources/js/script.js`) inside your theme folder.
|
||||||
|
Then registering it in `theme.properties` by adding:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
scripts=js/script.js
|
||||||
|
----
|
||||||
|
|
||||||
|
The `scripts` property supports a space separated list so you can add as many as you want.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
scripts=js/script.js js/more-script.js
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Images
|
||||||
|
|
||||||
|
To make images available to the theme add them to `resources/img`.
|
||||||
|
They can then be used through stylesheets.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
body {
|
||||||
|
background-image: url('../img/image.jpg');
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Or in templates, for example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
<img src="${url.resourcesPath}/img/image.jpg">
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Messages
|
||||||
|
|
||||||
|
Text in the templates are loaded from message bundles.
|
||||||
|
A theme that extends another theme will inherit all messages from the parents message bundle, but can override individual messages.
|
||||||
|
For example to replace `Username` on the login form with `Your Username` create the file `messages/messages.properties` inside your theme folder and add the following content:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
username=Your Username
|
||||||
|
----
|
||||||
|
|
||||||
|
For the admin console, there is a second resource bundle named `admin-messages.properties`.
|
||||||
|
This resource bundle is converted to JSON and shipped to the console to be processed by angular-translate.
|
||||||
|
It is found in the same directory as messages.properties and can be overridden in the same way as described above.
|
||||||
|
|
||||||
|
=== Modifying HTML
|
||||||
|
|
||||||
|
Keycloak uses http://freemarker.org[Freemarker Templates] in order to generate HTML.
|
||||||
|
These templates are defined in `.ftl` files and can be overriden from the base theme.
|
||||||
|
Check out the Freemarker website on how to form a template file.
|
||||||
|
To override the login template for the `example-theme` copy `themes/base/login/login.ftl` to `themes/example-theme/login` and open it in an editor.
|
||||||
|
After the first line (<#import ...>) add `<h1>HELLO WORLD!</h1>` then refresh the page.
|
||||||
|
|
||||||
|
== Deploying themes
|
||||||
|
|
||||||
|
Themes can be deployed to Keycloak by copying the theme directory to `themes` or it can be deployed as a module.
|
||||||
|
For a single server or during development just copying the theme is fine, but in a cluster or domain it's recommended to deploy as a module.
|
||||||
|
|
||||||
|
To deploy a theme as a module you need to create an jar (it's basically just a zip with jar extension) with the theme resources and a file `META/keycloak-themes.json` that describes the themes contained in the archive.
|
||||||
|
For example `example-theme.jar` with the contents:
|
||||||
|
|
||||||
|
* META-INF/keycloak-themes.json
|
||||||
|
* theme/example-theme/login/theme.properties
|
||||||
|
* theme/example-theme/login/login.ftl
|
||||||
|
* theme/example-theme/login/resources/css/styles.css
|
||||||
|
|
||||||
|
The contents of META-INF/keycloak-themes.json in this case would be:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"themes": [{
|
||||||
|
"name" : "example-theme",
|
||||||
|
"types": [ "login" ]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
----
|
||||||
|
As you can see a single jar can contain multiple themes and each theme can support one or more types.
|
||||||
|
|
||||||
|
The deploy the jar as a module to Keycloak you can either manually create the module or use `jboss-cli`.
|
||||||
|
It's simplest to use `jboss-cli` as it creates the required directories and module descriptor for you.
|
||||||
|
|
||||||
|
To deploy the above jar `jboss-cli` run:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
[
|
||||||
|
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.example.exampletheme --resources=example-theme.jar"
|
||||||
|
----
|
||||||
|
If you're on windows run
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
KEYCLOAK_HOME/bin/jboss-cli.bat
|
||||||
|
----
|
||||||
|
This command creates `modules/org/example/exampletheme/main` containing `example-theme.jar` and `module.xml`.
|
||||||
|
Once you've created the module you need to register it with Keycloak do this by editing `../standalone/configuration/keycloak-server.json` and adding the module to `theme/module/modules`.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
[
|
||||||
|
"theme": {
|
||||||
|
...
|
||||||
|
"module": {
|
||||||
|
"modules": [ "org.example.exampletheme" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
If a theme is deployed to `themes` and as a module the first is used.
|
||||||
|
|
||||||
|
== SPIs
|
||||||
|
|
||||||
|
For full control of login forms and account management Keycloak provides a number of SPIs.
|
||||||
|
|
||||||
|
=== Account SPI
|
||||||
|
|
||||||
|
The Account SPI allows implementing the account management pages using whatever web framework or templating engine you want.
|
||||||
|
To create an Account provider implement `org.keycloak.account.AccountProviderFactory` and `org.keycloak.account.AccountProvider`.
|
||||||
|
|
||||||
|
Once you have deployed your account provider to Keycloak you need to configure `keycloak-server.json` to specify which provider should be used:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
"account": {
|
||||||
|
"provider": "custom-provider"
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Login SPI
|
||||||
|
|
||||||
|
The Login SPI allows implementing the login forms using whatever web framework or templating engine you want.
|
||||||
|
To create a Login forms provider implement `org.keycloak.login.LoginFormsProviderFactory` and `org.keycloak.login.LoginFormsProvider` in `forms/login-api`.
|
||||||
|
|
||||||
|
Once you have deployed your account provider to Keycloak you need to configure `keycloak-server.json` to specify which provider should be used:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
|
||||||
|
"login": {
|
||||||
|
"provider": "custom-provider"
|
||||||
|
}
|
||||||
|
----
|
178
topics/user-federation.adoc
Executable file
|
@ -0,0 +1,178 @@
|
||||||
|
[[_user_federation]]
|
||||||
|
= User Federation SPI and LDAP/AD Integration
|
||||||
|
|
||||||
|
Keycloak can federate external user databases.
|
||||||
|
Out of the box we have support for LDAP and Active Directory.
|
||||||
|
Before you dive into this, you should understand how Keycloak does federation.
|
||||||
|
|
||||||
|
Keycloak performs federation a bit differently than other products/projects.
|
||||||
|
The vision of Keycloak is that it is an out of the box solution that should provide a core set of feature irregardless of the backend user storage you want to use.
|
||||||
|
Because of this requirement/vision, Keycloak has a set data model that all of its services use.
|
||||||
|
Most of the time when you want to federate an external user store, much of the metadata that would be needed to provide this complete feature set does not exist in that external store.
|
||||||
|
For example your LDAP server may only provide password validation, but not support TOTP or user role mappings.
|
||||||
|
The Keycloak User Federation SPI was written to support these completely variable configurations.
|
||||||
|
|
||||||
|
The way user federation works is that Keycloak will import your federated users on demand to its local storage.
|
||||||
|
How much metadata that is imported depends on the underlying federation plugin and how that plugin is configured.
|
||||||
|
Some federation plugins may only import the username into Keycloak storage, others might import everything from name, address, and phone number, to user role mappings.
|
||||||
|
Some plugins might want to import credentials directly into Keycloak storage and let Keycloak handle credential validation.
|
||||||
|
Others might want to handle credential validation themselves.
|
||||||
|
The goal of the Federation SPI is to support all of these scenarios.
|
||||||
|
|
||||||
|
== LDAP and Active Directory Plugin
|
||||||
|
|
||||||
|
Keycloak comes with a built-in LDAP/AD plugin.
|
||||||
|
By default, it is set up only to import username, email, first and last name, but you are free to configure <<_ldap_mappers,mappers>> and add more attributes or delete default ones.
|
||||||
|
It supports password validation via LDAP/AD protocols and different user metadata synchronization modes.
|
||||||
|
To configure a federated LDAP store go to the admin console.
|
||||||
|
Click on the `Users` menu option to get you to the user management page.
|
||||||
|
Then click on the `Federation` submenu option.
|
||||||
|
When you get to this page there is an "Add Provider" select box.
|
||||||
|
You should see "ldap" within this list.
|
||||||
|
Selecting "ldap" will bring you to the ldap configuration page.
|
||||||
|
|
||||||
|
=== Edit Mode
|
||||||
|
|
||||||
|
Edit mode defines various synchronization options with your LDAP store depending on what privileges you have.
|
||||||
|
|
||||||
|
READONLY::
|
||||||
|
Username, email, first and last name and other mapped attributes will be unchangeable.
|
||||||
|
Keycloak will show an error anytime anybody tries to update these fields.
|
||||||
|
Also, password updates will not be supported.
|
||||||
|
|
||||||
|
WRITABLE::
|
||||||
|
Username, email, first and last name, other mapped attributes and passwords can all be updated and will be synchronized automatically with your LDAP store.
|
||||||
|
|
||||||
|
UNSYNCED::
|
||||||
|
Any changes to username, email, first and last name, and passwords will be stored in Keycloak local storage.
|
||||||
|
It is up to you to figure out how to synchronize back to LDAP.
|
||||||
|
|
||||||
|
=== Other config options
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Display Name::
|
||||||
|
Name used when this provider is referenced in the admin console
|
||||||
|
|
||||||
|
Priority::
|
||||||
|
The priority of this provider when looking up users or for adding registrations.
|
||||||
|
|
||||||
|
Sync Registrations::
|
||||||
|
If a new user is added through a registration page or admin console, should the user be eligible to be synchronized to this provider.
|
||||||
|
|
||||||
|
Allow Kerberos authentication::
|
||||||
|
Enable Kerberos/SPNEGO authentication in realm with users data provisioned from LDAP.
|
||||||
|
More info in <<_kerberos,Kerberos section>>.
|
||||||
|
|
||||||
|
Other options::
|
||||||
|
The rest of the configuration options should be self explanatory.
|
||||||
|
You can use tooltips in admin console to see some more details about them.
|
||||||
|
|
||||||
|
=== Connect to LDAP over SSL
|
||||||
|
|
||||||
|
When you configure secured connection URL to LDAP (for example `ldaps://myhost.com:636` ) the Keycloak will use SSL for the communication with LDAP server.
|
||||||
|
The important thing is to properly configure truststore on the Keycloak server side, because SSL won't work if Keycloak can't trust the SSL connection with LDAP (Keycloak acts as the `client` here, when LDAP acts as server).
|
||||||
|
|
||||||
|
The global truststore for the Keycloak can be configured with Truststore SPI in the `keycloak-server.json` file and it's described in the details <<_truststore,here>>.
|
||||||
|
If you don't configure truststore SPI, the truststore will fallback to the default mechanism provided by Java (either the file provided by system property `javax.net.ssl.trustStore` or finally the cacerts file from JDK if even the system property is not set).
|
||||||
|
|
||||||
|
There is configuration property `Use Truststore SPI` in the LDAP federation provider configuration, where you can choose whether Truststore SPI is used.
|
||||||
|
By default, the value is `ldaps only`, which is fine for most of deployments, because attempt to use Truststore SPI is done just if connection to LDAP starts with `ldaps` .
|
||||||
|
|
||||||
|
== Sync of LDAP users to Keycloak
|
||||||
|
|
||||||
|
LDAP Federation Provider will automatically take care of synchronization (import) of needed LDAP users into Keycloak database.
|
||||||
|
For example once you first authenticate LDAP user `john` from Keycloak UI, LDAP Federation provider will first import this LDAP user into Keycloak database and then authenticate against LDAP password.
|
||||||
|
|
||||||
|
Federation Provider imports just requested users by default, so if you click to `View all users` in Keycloak admin console, you will see just those LDAP users, which were already authenticated/requested by Keycloak.
|
||||||
|
|
||||||
|
If you want to sync all LDAP users into Keycloak database, you may configure and enable Sync, which is in admin console on same page like the configuration of Federation provider itself.
|
||||||
|
There are 2 types of sync:
|
||||||
|
|
||||||
|
Full sync::
|
||||||
|
This will synchronize all LDAP users into Keycloak DB.
|
||||||
|
Those LDAP users, which already exist in Keycloak and were changed in LDAP directly will be updated in Keycloak DB (For example if user `Mary Kelly` was changed in LDAP to `Mary Doe`).
|
||||||
|
|
||||||
|
Changed users sync::
|
||||||
|
This will check LDAP and it will sync into Keycloak just those users, which were created or updated in LDAP from the time of last sync.
|
||||||
|
|
||||||
|
In usual cases you may want to trigger full sync at the beginning, so you will import all LDAP users to Keycloak just once.
|
||||||
|
Then you may setup periodic sync of changed users, so Keycloak will periodically ask LDAP server for newly created or updated users and backport them to Keycloak DB.
|
||||||
|
Also you may want to trigger full sync again after some longer time or setup periodic full sync as well.
|
||||||
|
|
||||||
|
In admin console, you can trigger sync directly or you can enable periodic changed or full sync.
|
||||||
|
|
||||||
|
[[_ldap_mappers]]
|
||||||
|
== LDAP/Federation mappers
|
||||||
|
|
||||||
|
LDAP mappers are `listeners`, which are triggered by LDAP Federation provider at various points and provide another extension point to LDAP integration.
|
||||||
|
They are triggered during import LDAP user into Keycloak, registration Keycloak user back to LDAP or when querying LDAP user from Keycloak.
|
||||||
|
When you create LDAP Federation provider, Keycloak will automatically provide set of builtin `mappers` for this provider.
|
||||||
|
You are free to change this set and create new mapper or update/delete existing ones.
|
||||||
|
|
||||||
|
By default, we have those implementation of LDAP federation mapper:
|
||||||
|
|
||||||
|
User Attribute Mapper::
|
||||||
|
This allows to specify which LDAP attribute is mapped to which attribute of Keycloak User.
|
||||||
|
So for example you can configure that LDAP attribute `mail` is supposed to be mapped to the UserModel attribute `email` in Keycloak database.
|
||||||
|
For this mapper implementation, there is always one-to-one mapping (one LDAP attribute mapped to one Keycloak UserModel attribute)
|
||||||
|
|
||||||
|
FullName Mapper::
|
||||||
|
This allows to specify that fullname of user, which is saved in some LDAP attribute (usualy `cn` ) will be mapped to `firstName` and `lastname` attributes of UserModel.
|
||||||
|
Having `cn` to contain full name of user is common case for some LDAP deployments.
|
||||||
|
|
||||||
|
Role Mapper::
|
||||||
|
This allows to configure role mappings from LDAP into Keycloak role mappings.
|
||||||
|
One Role mapper can be used to map LDAP roles (usually groups from particular branch of LDAP tree) into roles corresponding to either realm roles or client roles of specified client.
|
||||||
|
It's not a problem to configure more Role mappers for same LDAP provider.
|
||||||
|
So for example you can specify that role mappings from groups under `ou=main,dc=example,dc=org` will be mapped to realm role mappings and role mappings from groups under `ou=finance,dc=example,dc=org` will be mapped to client role mappings of client `finance` .
|
||||||
|
|
||||||
|
Hardcoded Role Mapper::
|
||||||
|
This mapper will grant specified Keycloak role to each Keycloak user linked with LDAP.
|
||||||
|
|
||||||
|
Group Mapper::
|
||||||
|
This allows to configure group mappings from LDAP into Keycloak group mappings.
|
||||||
|
Group mapper can be used to map LDAP groups from particular branch of LDAP tree into groups in Keycloak.
|
||||||
|
And it will also propagate user-group mappings from LDAP into user-group mappings in Keycloak.
|
||||||
|
|
||||||
|
MSAD User Account Mapper::
|
||||||
|
Mapper specific to Microsoft Active Directory (MSAD). It's able to tightly integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc). It's using `userAccountControl` and `pwdLastSet` LDAP attributes for that (both are specific to MSAD and are not LDAP standard). For example if pwdLastSet is 0, the Keycloak user is required to update password (there will be UPDATE_PASSWORD required action added to him in Keycloak). Or if userAccountControl is 514 (disabled account) the Keycloak user is disabled as well etc.
|
||||||
|
|
||||||
|
By default, there is set of User Attribute mappers to map basic UserModel attributes username, first name, lastname and email to corresponding LDAP attributes.
|
||||||
|
You are free to extend this and provide more attribute mappings (For example to street, postalCode etc), delete firstName/lastname mapper and put fullName mapper instead, add role mappers etc.
|
||||||
|
Admin console provides tooltips, which should help on how to configure corresponding mappers.
|
||||||
|
|
||||||
|
We have an example, which is showing LDAP integration and set of base mappers and sample mappers (mappers for street and postalCode) . It's in `examples/ldap` in the Keycloak example distribution or demo distribution download.
|
||||||
|
You can also check the example sources directly https://github.com/keycloak/keycloak/blob/master/examples/ldap[here] .
|
||||||
|
|
||||||
|
=== Writing your own LDAP Mapper
|
||||||
|
|
||||||
|
For the more advanced usecases, you have the possibility to create your own implementation of LDAP mapper or just subclass from some already existing mapper implementation.
|
||||||
|
You will need to implement `UserFederationMapperFactory` interface.
|
||||||
|
In most cases, instead of creating `UserFederationMapperFactory` from scratch, you can create subclasses of `AbstractLDAPFederationMapperFactory`, which itself implements `UserFederationMapperFactory`.
|
||||||
|
Then you need to create mapper implementation, which will be subclass of `AbstractLDAPFederationMapper` (this mapper implementation will be returned by `YourAbstractLDAPFederationMapperFactorySubclass.createMapper` method).
|
||||||
|
|
||||||
|
After your code is written you must package up all your classes within a JAR file.
|
||||||
|
This jar file must contain a file called `org.keycloak.mappers.UserFederationMapperFactory` within the `META-INF/services directory` of the JAR.
|
||||||
|
This file is a list of fully qualified classnames of all implementations of `UserFederationMapperFactory`.
|
||||||
|
For more details, look at section for <<_write_federation_provider,Write your own federation provider>> and at <<_providers,Providers and SPI>> section.
|
||||||
|
|
||||||
|
[[_write_federation_provider]]
|
||||||
|
== Writing your own User Federation Provider
|
||||||
|
|
||||||
|
The keycloak examples directory contains an example of a simple User Federation Provider backed by a simple properties file.
|
||||||
|
See `examples/providers/federation-provider`.
|
||||||
|
Most of how to create a federation provider is explained directly within the example code, but some information is here too.
|
||||||
|
|
||||||
|
Writing a User Federation Provider starts by implementing the `UserFederationProvider` and `UserFederationProviderFactory` interfaces.
|
||||||
|
Please see the Javadoc and example for complete details on how to do this.
|
||||||
|
Some important methods of note: getUserByUsername() and getUserByEmail() require that you query your federated storage and if the user exists create and import the user into Keycloak storage.
|
||||||
|
How much metadata you import is fully up to you.
|
||||||
|
This import is done by invoking methods on the object returned `KeycloakSession.userStorage()` to add and import user information.
|
||||||
|
The proxy() method will be called whenever Keycloak has found an imported UserModel.
|
||||||
|
This allows the federation provider to proxy the UserModel which is useful if you want to support external storage updates on demand.
|
||||||
|
|
||||||
|
After your code is written you must package up all your classes within a JAR file.
|
||||||
|
This jar file must contain a file called `org.keycloak.models.UserFederationProviderFactory` within the `META-INF/services` directory of the JAR.
|
||||||
|
This file is a list of fully qualified classnames of all implementations of `UserFederationProviderFactory`.
|
||||||
|
For more details on writing provider implementations and how to deploy to Keycloak refer to the <<_providers,providers>> section.
|