770 lines
No EOL
45 KiB
XML
Executable file
770 lines
No EOL
45 KiB
XML
Executable file
<chapter id="auth_spi">
|
|
<title>Custom Authentication, Registration, and Required Actions</title>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<section>
|
|
<title>Terms</title>
|
|
<para>
|
|
To first learn about the Authentication SPI, let's go over some of the terms used to describe it.
|
|
<variablelist>
|
|
<varlistentry>
|
|
<term>Authentication Flow</term>
|
|
<listitem>
|
|
<para>
|
|
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 granta access, and registration.
|
|
</para>
|
|
</listitem>
|
|
</varlistentry>
|
|
<varlistentry>
|
|
<term>Authenticator</term>
|
|
<listitem>
|
|
<para>
|
|
An authenticator is a pluggable component that hold the logic for performing the authentication
|
|
or action within a flow. It is usually a singleton.
|
|
</para>
|
|
</listitem>
|
|
</varlistentry>
|
|
<varlistentry>
|
|
<term>Execution</term>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
</varlistentry>
|
|
<varlistentry>
|
|
<term>Execution Requirement</term>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
</varlistentry>
|
|
<varlistentry>
|
|
<term>Authenticator Config</term>
|
|
<listitem>
|
|
<para>
|
|
This object defines the configuration for the Authenticator for a specific execution within
|
|
an authentication flow. Each execution can have a different config.
|
|
</para>
|
|
</listitem>
|
|
</varlistentry>
|
|
<varlistentry>
|
|
<term>Required Action</term>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
</varlistentry>
|
|
</variablelist>
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Algorithm Overview</title>
|
|
<para>
|
|
Let's talk about how this all works for browser login. Let's assume the following flows, executions and sub flows.
|
|
<programlisting><![CDATA[
|
|
Cookie - ALTERNATIVE
|
|
Kerberos - ALTERNATIVE
|
|
Forms Subflow - ALTERNATIVE
|
|
Username/Password Form - REQUIRED
|
|
OTP Password Form - OPTIONAL
|
|
]]>
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
<orderedlist>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
The next execution is a subflow called Forms. The executions for this subflow are loaded and
|
|
the same processing logic occurs
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
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()
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
If the required action is ultimately successful, then the required action is removed from the user's require actions list.
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
After all required actions have been resolved, the user is finally logged in.
|
|
</para>
|
|
</listitem>
|
|
</orderedlist>
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Authenticator SPI Walk Through</title>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<section>
|
|
<title>Packaging Classes and Deployment</title>
|
|
<para>
|
|
You will package your classes within a single jar. This jar must contain a file named <literal>org.keycloak.authentication.AuthenticatorFactory</literal>
|
|
and must be contained in the <literal>META-INF/services/</literal> directory of your jar. This file must list the fully qualified classname
|
|
of each AuthenticatorFactory implementation you have in the jar. For example:
|
|
<programlisting>
|
|
org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
|
|
org.keycloak.examples.authenticator.AnotherProviderFactory
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
This services/ file is used by Keycloak to scan the providers it has to load into the system.
|
|
</para>
|
|
<para>
|
|
To deploy this jar, just copy it to the standalone/configuration/providers directory.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Implementing an Authenticator</title>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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 of the answer to the
|
|
secret question is 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:
|
|
<programlisting>
|
|
@Override
|
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
|
return session.users().configuredForCredentialType("secret_question", realm, user);
|
|
}
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
The configuredForCredentialType() call queries the user to see if it supports that credential type.
|
|
</para>
|
|
<para>
|
|
The next method to implement on the Authenticator is setRequiredActions(). If configuredFor() returns fales
|
|
and our example authenticator is required within the flow, this method will be called. It is response 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.
|
|
<programlisting>
|
|
@Override
|
|
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
|
user.addRequiredAction("SECRET_QUESTION_CONFIG");
|
|
}
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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, that the
|
|
user doesn't have to answer the question again. Basically 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.
|
|
<programlisting>
|
|
@Override
|
|
public void authenticate(AuthenticationFlowContext context) {
|
|
if (hasCookie(context)) {
|
|
context.success();
|
|
return;
|
|
}
|
|
Response challenge = loginForm(context).createForm("secret_question.ftl");
|
|
context.challenge(challenge);
|
|
}
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
If 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.
|
|
</para>
|
|
<para>
|
|
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 <literal>org.keycloak.login.LoginFormsProvider</literal>.
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
<programlisting>
|
|
@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();
|
|
}
|
|
|
|
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
If validation is successful, then we set a cookie to remember that the secret question has been answered
|
|
and we call AuthenticationFlowContext.success().
|
|
</para>
|
|
<para>
|
|
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.
|
|
<programlisting>
|
|
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 ...
|
|
}
|
|
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Implementing an AuthenticatorFactory</title>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
|
|
<programlisting>
|
|
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;
|
|
}
|
|
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
<programlisting>
|
|
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
|
AuthenticationExecutionModel.Requirement.REQUIRED,
|
|
AuthenticationExecutionModel.Requirement.DISABLED
|
|
};
|
|
@Override
|
|
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
|
return REQUIREMENT_CHOICES;
|
|
}
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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().
|
|
<programlisting>
|
|
@Override
|
|
public boolean isUserSetupAllowed() {
|
|
return true;
|
|
}
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
<programlisting><![CDATA[
|
|
@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);
|
|
}
|
|
]]></programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Adding Authenticator Form</title>
|
|
<para>
|
|
Keycloak comes with a Freemarker <link linkend="themes">theme and template engine</link>. The createForm()
|
|
method you called within authenticate() of your Authenticator class, builds an HTML page from a file within
|
|
your login them: secret-question.ftl. This file should be placed in the login theme with all the other
|
|
.ftl files you see for login.
|
|
</para>
|
|
<para>
|
|
Let's take a bigger look at secret-question.ftl Here's a small code snippet:
|
|
<programlisting><![CDATA[
|
|
<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>
|
|
|
|
]]></programlisting>
|
|
</para>
|
|
<para>
|
|
Any piece of text enclosed in <literal>${}</literal> corresponds to an attribute or template funtion.
|
|
If you see the form's action, you see it points to <literal>${url.loginAction}</literal>. 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.
|
|
</para>
|
|
<para>
|
|
You'll also see <literal>${properties.someValue}</literal>. These correspond to properties defined
|
|
in your theme.properties file of our theme. <literal>${msg("someValue")}</literal> 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 <literal>loginSecretQuestion</literal>
|
|
value. This should be the question you want to ask the user.
|
|
</para>
|
|
<para>
|
|
When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvider instance. If you called,
|
|
<literal>LoginFormsProvider.setAttribute("foo", "bar")</literal>, the value of "foo" would be available
|
|
for reference in your form as <literal>${foo}</literal>. The value of an attribute can be any Java
|
|
bean as well.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Adding Authenticator to a Flow</title>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</section>
|
|
</section>
|
|
<section>
|
|
<title>Required Action Walkthrough</title>
|
|
<para>
|
|
In this section 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.
|
|
</para>
|
|
<section>
|
|
<title>Packaging Classes and Deployment</title>
|
|
<para>
|
|
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 <literal>org.keycloak.authentication.RequiredActionFactory</literal>
|
|
and must be contained in the <literal>META-INF/services/</literal> directory of your jar. This file must list the fully qualified classname
|
|
of each RequiredActionFactory implementation you have in the jar. For example:
|
|
<programlisting>
|
|
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
This services/ file is used by Keycloak to scan the providers it has to load into the system.
|
|
</para>
|
|
<para>
|
|
To deploy this jar, just copy it to the standalone/configuration/providers directory.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Implement the RequiredActionProvider</title>
|
|
<para>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.
|
|
<programlisting>
|
|
@Override
|
|
public void requiredActionChallenge(RequiredActionContext context) {
|
|
Response challenge = context.form().createForm("secret_question_config.ftl");
|
|
context.challenge(challenge);
|
|
|
|
}
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
The challenge() method notifies the flow manager that
|
|
a required action must be executed.
|
|
</para>
|
|
<para>
|
|
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
|
|
<programlisting>
|
|
@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();
|
|
}
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Implement the RequiredActionFactory</title>
|
|
<para>
|
|
This class is really simple. It is just responsible for creating the required actin provider instance.
|
|
<programlisting>
|
|
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";
|
|
}
|
|
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
The getDisplayText() method is just for the admin console when it wants to display a friendly name
|
|
for the required action.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Enable Required Action</title>
|
|
<para>
|
|
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. Find your required action, and enable. Alternatively, if you
|
|
click on the default action checkbox, this required action will be applied anytime a new user is created.
|
|
</para>
|
|
</section>
|
|
</section>
|
|
<section>
|
|
<title>Modifying/Extending the Registration Form</title>
|
|
<para>
|
|
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 be 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 the implementation of the recaptcha support that
|
|
Keycloak provides out of the box to show you how to do this.
|
|
</para>
|
|
<section>
|
|
<title>Implementation FormAction Interface</title>
|
|
<para>
|
|
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()
|
|
<programlisting><![CDATA[
|
|
@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");
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
The 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.
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
Recaptcha also has the requirement of loading a javascript script. You can do this by calling LoginFormsProvider.addScript()
|
|
passing in the URL.
|
|
</para>
|
|
<para>
|
|
The next meaty part of this interface is the validate() method. This is called immediately upon receiving a form
|
|
post.
|
|
<programlisting><![CDATA[
|
|
@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;
|
|
|
|
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
After all validations have been processed then, the form flow then invokes the FormAction.success() method. For recaptcha
|
|
this is a no-op, but if you have additional metadata you want to add to UserModel, you can do that in success() method.
|
|
</para>
|
|
<para>
|
|
Finally the FormActionFactory class is really implemented similarly to AuthenticatorFactory, so we won't go over it.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Packaging the Action</title>
|
|
<para>
|
|
You will package your classes within a single jar. This jar must contain a file named <literal>org.keycloak.authentication.ForActionFactory</literal>
|
|
and must be contained in the <literal>META-INF/services/</literal> directory of your jar. This file must list the fully qualified classname
|
|
of each FormActionFactory implementation you have in the jar. For example:
|
|
<programlisting>
|
|
org.keycloak.examples.authenticator.registration.RecaptchaFormActionFactory
|
|
</programlisting>
|
|
</para>
|
|
<para>
|
|
This services/ file is used by Keycloak to scan the providers it has to load into the system.
|
|
</para>
|
|
<para>
|
|
To deploy this jar, just copy it to the standalone/configuration/providers directory.
|
|
</para>
|
|
</section>
|
|
<section>
|
|
<title>Adding FormAction to the Registration Flow</title>
|
|
<para>
|
|
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.
|
|
</para>
|
|
<para>
|
|
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".
|
|
</para>
|
|
<para>
|
|
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.
|
|
</para>
|
|
</section>
|
|
</section>
|
|
</chapter> |