1f772d2957
Signed-off-by: stianst <stianst@gmail.com>
1228 lines
64 KiB
Text
1228 lines
64 KiB
Text
[[_auth_spi]]
|
|
== Authentication SPI
|
|
|
|
|
|
{project_name} includes a range of different authentication mechanisms: kerberos, password, otp and others.
|
|
These mechanisms may not meet all of your requirements and you may want to plug in your own custom ones.
|
|
{project_name} provides an authentication SPI that you can use to write new plugins.
|
|
The Admin Console supports applying, ordering, and configuring these new mechanisms.
|
|
|
|
{project_name} also supports a simple registration form.
|
|
Different aspects of this form can be enabled and disabled for example
|
|
reCAPTCHA support can be turned off and on.
|
|
The same authentication SPI can be used to add another page to the registration flow or reimplement it entirely.
|
|
There's also an additional fine-grained SPI you can use to add specific validations and user extensions to the built-in registration form.
|
|
|
|
A required action in {project_name} 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.
|
|
{project_name} 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.
|
|
|
|
WARNING: If your authenticator or required action implementation is using some user attributes as the metadata attributes for linking/establishing the user identity,
|
|
then please make sure that users are not able to edit the attributes and the corresponding attributes are read-only. See the details in the link:{adminguide_link}#read_only_user_attributes[Threat model mitigation chapter].
|
|
|
|
=== Terms
|
|
|
|
To first learn about the Authentication SPI, let's go over some of the terms used to describe it.
|
|
|
|
Authentication Flow::
|
|
A flow is a container for all authentications that must happen during login or registration.
|
|
If you go to the Admin Console authentication page, you can view all the defined flows in the system and what authenticators they are made up of.
|
|
Flows can contain other flows.
|
|
You can also bind a new different flow for browser login, direct grant access, and registration.
|
|
|
|
Authenticator::
|
|
An authenticator is a pluggable component that hold the logic for performing the authentication or action within a flow.
|
|
It is usually a singleton.
|
|
|
|
Execution::
|
|
An execution is an object that binds the authenticator to the flow and the authenticator to the configuration of the authenticator.
|
|
Flows contain execution entries.
|
|
|
|
Execution Requirement::
|
|
Each execution defines how an authenticator behaves in a flow.
|
|
The requirement defines whether the authenticator is enabled, disabled, conditional, required, or an alternative.
|
|
An alternative requirement means that the authenticator is enough to validate the flow it's in, but isn't necessary.
|
|
For example, in the built-in browser flow, cookie authentication, the Identity Provider Redirector, and the set of all authenticators in the
|
|
forms subflow are all alternative. As they are executed in a sequential top-to-bottom order, if one of them is successful, the flow is
|
|
successful, and any following execution in the flow (or sub-flow) is not evaluated.
|
|
|
|
Authenticator Config::
|
|
This object defines the configuration for the Authenticator for a specific execution within an authentication flow.
|
|
Each execution can have a different config.
|
|
|
|
Required Action::
|
|
After authentication completes, the user might have one or more one-time actions he must complete before he is allowed to login.
|
|
The user might be required to set up an OTP token generator or reset an expired password or even accept a Terms and Conditions document.
|
|
|
|
=== Algorithm overview
|
|
|
|
Let's talk about how this all works for browser login.
|
|
Let's assume the following flows, executions and sub flows.
|
|
[source]
|
|
----
|
|
|
|
Cookie - ALTERNATIVE
|
|
Kerberos - ALTERNATIVE
|
|
Forms subflow - ALTERNATIVE
|
|
Username/Password Form - REQUIRED
|
|
Conditional OTP subflow - CONDITIONAL
|
|
Condition - User Configured - REQUIRED
|
|
OTP Form - REQUIRED
|
|
----
|
|
|
|
In the top level of the form we have 3 executions of which all are alternatively required.
|
|
This means that if any of these are successful, then the others do not have to execute.
|
|
The Username/Password form is not executed if there is an SSO Cookie set or a successful Kerberos login.
|
|
Let's walk through the steps from when a client first redirects to keycloak to authenticate the user.
|
|
|
|
. The OpenID Connect or SAML protocol provider unpacks relevant data, verifies the client and any signatures.
|
|
It creates an AuthenticationSessionModel.
|
|
It looks up what the browser flow should be, then starts executing the flow.
|
|
. The flow looks at the cookie execution and sees that it is an alternative.
|
|
It loads the cookie provider.
|
|
It checks to see if the cookie provider requires that a user already be associated with the authentication session.
|
|
Cookie provider does not require a user.
|
|
If it did, the flow would abort and the user would see an error screen.
|
|
Cookie provider then executes.
|
|
Its purpose is to see if there is an SSO cookie set.
|
|
If there is one set, it is validated and the UserSessionModel is verified and associated with the AuthenticationSessionModel.
|
|
The Cookie provider returns a success() status if the SSO cookie exists and is validated.
|
|
Since the cookie provider returned success and each execution at this level of the flow is ALTERNATIVE, no other execution is executed and this results in a successful login.
|
|
If there is no SSO cookie, the cookie provider returns with a status of attempted(). This means there was no error condition, but no success either.
|
|
The provider tried, but the request just wasn't set up to handle this authenticator.
|
|
. Next the flow looks at the Kerberos execution.
|
|
This is also an alternative.
|
|
The kerberos provider also does not require a user to be already set up and associated with the AuthenticationSessionModel so this provider is executed.
|
|
Kerberos uses the SPNEGO browser protocol.
|
|
This requires a series of challenge/responses between the server and client exchanging negotiation headers.
|
|
The kerberos provider does not see any negotiate header, so it assumes that this is the first interaction between the server and client.
|
|
It therefore creates an HTTP challenge response to the client and sets a forceChallenge() status.
|
|
A forceChallenge() means that this HTTP response cannot be ignored by the flow and must be returned to the client.
|
|
If instead the provider returned a challenge() status, the flow would hold the challenge response until all other alternatives are attempted.
|
|
So, in this initial phase, the flow would stop and the challenge response would be sent back to the browser.
|
|
If the browser then responds with a successful negotiate header, the provider associates the user with the AuthenticationSession and the flow ends because the rest of the executions on this level of the flow are all alternatives.
|
|
Otherwise, again, the kerberos provider sets an attempted() status and the flow continues.
|
|
. The next execution is a subflow called Forms.
|
|
The executions for this subflow are loaded and the same processing logic occurs.
|
|
. The first execution in the Forms subflow is the UsernamePassword provider.
|
|
This provider also does not require for a user to already be associated with the flow.
|
|
This provider creates a challenge HTTP response and sets its status to challenge(). This execution is required, so the flow honors this challenge and sends the HTTP response back to the browser.
|
|
This response is a rendering of the Username/Password HTML page.
|
|
The user enters in their username and password and clicks submit.
|
|
This HTTP request is directed to the UsernamePassword provider.
|
|
If the user entered an invalid username or password, a new challenge response is created and a status of failureChallenge() is set for this execution.
|
|
A failureChallenge() means that there is a challenge, but that the flow should log this as an error in the error log.
|
|
This error log can be used to lock accounts or IP Addresses that have had too many login failures.
|
|
If the username and password is valid, the provider associated the UserModel with the AuthenticationSessionModel and returns a status of success().
|
|
. The next execution is a subflow called Conditional OTP. The executions for this subflow are loaded and the same processing logic occurs. Its Requirement is
|
|
Conditional. This means that the flow will first evaluate all conditional executors that it contains. Conditional executors are authenticators that
|
|
implement `ConditionalAuthenticator`, and must implement the method `boolean matchCondition(AuthenticationFlowContext context)`. A conditional subflow will
|
|
call the `matchCondition` method of all conditional executions it contains, and if all of them evaluate to true, it will act as if it was a required subflow. If
|
|
not, it will act as if it was a disabled subflow. Conditional authenticators are only used for this purpose, and are not used as authenticators.
|
|
This means that even if the conditional authenticator evaluates to "true", then this will not mark a flow or subflow as successful. For example,
|
|
a flow containing only a Conditional subflow with only a conditional authenticator will never allow a user to log in.
|
|
. The first execution of the Conditional OTP subflow is the Condition - User Configured.
|
|
This provider requires that a user has been associated with the flow.
|
|
This requirement is satisfied because the UsernamePassword provider already associated the user with the flow.
|
|
This provider's `matchCondition` method will evaluate the `configuredFor` method for all other Authenticators in its current subflow. If the subflow contains
|
|
executors with their Requirement set to required, then the `matchCondition` method will only evaluate to true if all the required authenticators' `configuredFor`
|
|
method evaluate to true. Otherwise, the `matchCondition` method will evaluate to true if any alternative authenticator evaluates to true.
|
|
. The next execution is the OTP Form.
|
|
This provider also requires that a user has been associated with the flow.
|
|
This requirement is satisfied because the UsernamePassword provider already associated the user with the flow.
|
|
Since a user is required for this provider, the provider is also asked if the user is configured to use this provider.
|
|
If user is not configured, then the flow will then set up a required action that the user must perform after authentication is complete.
|
|
For OTP, this means the OTP setup page. If the user is configured, he will be asked to enter his otp code. In our scenario, because of the conditional
|
|
sub-flow, the user will never see the OTP login page, unless the Conditional OTP subflow is set to Required.
|
|
. After the flow is complete, the authentication processor creates a UserSessionModel and associates it with the AuthenticationSessionModel.
|
|
It then checks to see if the user is required to complete any required actions before logging in.
|
|
. First, each required action's evaluateTriggers() method is called.
|
|
This allows the required action provider to figure out if there is some state that might trigger the action to be fired.
|
|
For example, if your realm has a password expiration policy, it might be triggered by this method.
|
|
. Each required action associated with the user that has its requiredActionChallenge() method called.
|
|
Here the provider sets up an HTTP response which renders the page for the required action.
|
|
This is done by setting a challenge status.
|
|
. If the required action is ultimately successful, then the required action is removed from the user's required actions list.
|
|
. After all required actions have been resolved, the user is finally logged in.
|
|
|
|
[[_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 {quickstartRepo_link}[{quickstartRepo_name}] repository under `extension/authenticator`.
|
|
|
|
To create an authenticator, you must at minimum implement the org.keycloak.authentication.AuthenticatorFactory and Authenticator interfaces.
|
|
The Authenticator interface defines the logic. The AuthenticatorFactory is responsible for creating instances of an Authenticator.
|
|
They both extend a more generic Provider and ProviderFactory set of interfaces that other {project_name} components like User Federation do.
|
|
|
|
Some authenticators, like the CookieAuthenticator don't rely on a Credential that the user has or knows to authenticate the user.
|
|
However, some authenticators, such as the PasswordForm authenticator or the OTPFormAuthenticator rely on the user inputting some
|
|
information and verifying that information against some information in the
|
|
database. For the PasswordForm for example, the authenticator will verify the hash of the password against a hash stored in the database, while the
|
|
OTPFormAuthenticator will verify the OTP received against the one generated from the shared secret stored in the database.
|
|
|
|
These types of authenticators are called CredentialValidators, and will require you to implement a few more classes:
|
|
|
|
* A class that extends org.keycloak.credential.CredentialModel, and that can generate the correct format of the credential in the database
|
|
* A class implementing the org.keycloak.credential.CredentialProvider and interface, and a class implementing its CredentialProviderFactory factory interface.
|
|
|
|
The SecretQuestionAuthenticator we'll see in this walk through is a CredentialValidator, so we'll see how to implement all these classes.
|
|
|
|
==== Packaging classes and deployment
|
|
|
|
You will package your classes within a single jar.
|
|
This jar must contain a file named `org.keycloak.authentication.AuthenticatorFactory` and must be contained in the `META-INF/services/` directory of your jar.
|
|
This file must list the fully qualified class name of each AuthenticatorFactory implementation you have in the jar.
|
|
For example:
|
|
|
|
[source,java]
|
|
----
|
|
org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
|
|
org.keycloak.examples.authenticator.AnotherProviderFactory
|
|
----
|
|
|
|
This services/ file is used by {project_name} to scan the providers it has to load into the system.
|
|
|
|
To deploy this jar, just copy it to the providers directory.
|
|
|
|
==== Extending the CredentialModel class
|
|
|
|
In {project_name}, credentials are stored in the database in the Credentials table. It has the following structure:
|
|
|
|
----
|
|
-----------------------------
|
|
| ID |
|
|
-----------------------------
|
|
| user_ID |
|
|
-----------------------------
|
|
| credential_type |
|
|
-----------------------------
|
|
| created_date |
|
|
-----------------------------
|
|
| user_label |
|
|
-----------------------------
|
|
| secret_data |
|
|
-----------------------------
|
|
| credential_data |
|
|
-----------------------------
|
|
| priority |
|
|
-----------------------------
|
|
----
|
|
|
|
Where:
|
|
|
|
* `ID` is the primary key of the credential.
|
|
* `user_ID` is the foreign key linking the credential to a user.
|
|
* `credential_type` is a string set during the creation that must reference an existing credential type.
|
|
* `created_date` is the creation timestamp (in long format) of the credential.
|
|
* `user_label` is the editable name of the credential by the user
|
|
* `secret_data` contains a static json with the information that cannot be transmitted outside of {project_name}
|
|
* `credential_data` contains a json with the static information of the credential that can be shared in the Admin Console or via the REST API.
|
|
* `priority` defines how "preferred" a credential is for a user, to determine which credential to present when a user has multiple choices.
|
|
|
|
As the secret_data and credential_data fields are designed to contain json, it is up to you to determine how to structure, read and write into
|
|
these fields, allowing you a lot of flexibility.
|
|
|
|
For this example, we are going to use a very simple credential data, containing only the question asked to the user:
|
|
|
|
[source]
|
|
----
|
|
{
|
|
"question":"aQuestion"
|
|
}
|
|
----
|
|
|
|
with an equally simple secret data, containing only the secret answer:
|
|
|
|
[source]
|
|
----
|
|
{
|
|
"answer":"anAnswer"
|
|
}
|
|
----
|
|
|
|
Here the answer will be kept in plain text in the database for the sake of simplicity, but it would also be possible to have a salted hash for the answer,
|
|
as is the case for passwords in {project_name}. In this case, the secret data would also have to contain a field for the salt, and the credential data information
|
|
about the algorithm such as the type of algorithm used and the number of iterations used. For more details you can consult the implementation of the
|
|
`org.keycloak.models.credential.PasswordCredentialModel` class.
|
|
|
|
In our case we create the class `SecretQuestionCredentialModel`:
|
|
|
|
|
|
[source,java]
|
|
----
|
|
public class SecretQuestionCredentialModel extends CredentialModel {
|
|
public static final String TYPE = "SECRET_QUESTION";
|
|
|
|
private final SecretQuestionCredentialData credentialData;
|
|
private final SecretQuestionSecretData secretData;
|
|
----
|
|
|
|
Where `TYPE` is the credential_type we write in the database. For consistency, we make sure that this String is always the one referenced when
|
|
getting the type for this credential. The classes `SecretQuestionCredentialData` and `SecretQuestionSecretData` are used to marshal and unmarshal the json:
|
|
|
|
[source,java]
|
|
----
|
|
public class SecretQuestionCredentialData {
|
|
|
|
private final String question;
|
|
|
|
@JsonCreator
|
|
public SecretQuestionCredentialData(@JsonProperty("question") String question) {
|
|
this.question = question;
|
|
}
|
|
|
|
public String getQuestion() {
|
|
return question;
|
|
}
|
|
}
|
|
----
|
|
|
|
[source,java]
|
|
----
|
|
public class SecretQuestionSecretData {
|
|
|
|
private final String answer;
|
|
|
|
@JsonCreator
|
|
public SecretQuestionSecretData(@JsonProperty("answer") String answer) {
|
|
this.answer = answer;
|
|
}
|
|
|
|
public String getAnswer() {
|
|
return answer;
|
|
}
|
|
}
|
|
----
|
|
|
|
To be fully usable, the `SecretQuestionCredentialModel` objects must both contain the raw json data from its parent class,
|
|
and the unmarshalled objects in its own attributes. This leads us to create a method which reads from a simple CredentialModel,
|
|
such as is created when reading from the database, to make a `SecretQuestionCredentialModel`:
|
|
|
|
[source,java]
|
|
----
|
|
private SecretQuestionCredentialModel(SecretQuestionCredentialData credentialData, SecretQuestionSecretData secretData) {
|
|
this.credentialData = credentialData;
|
|
this.secretData = secretData;
|
|
}
|
|
|
|
public static SecretQuestionCredentialModel createFromCredentialModel(CredentialModel credentialModel){
|
|
try {
|
|
SecretQuestionCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), SecretQuestionCredentialData.class);
|
|
SecretQuestionSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), SecretQuestionSecretData.class);
|
|
|
|
SecretQuestionCredentialModel secretQuestionCredentialModel = new SecretQuestionCredentialModel(credentialData, secretData);
|
|
secretQuestionCredentialModel.setUserLabel(credentialModel.getUserLabel());
|
|
secretQuestionCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
|
|
secretQuestionCredentialModel.setType(TYPE);
|
|
secretQuestionCredentialModel.setId(credentialModel.getId());
|
|
secretQuestionCredentialModel.setSecretData(credentialModel.getSecretData());
|
|
secretQuestionCredentialModel.setCredentialData(credentialModel.getCredentialData());
|
|
return secretQuestionCredentialModel;
|
|
} catch (IOException e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
----
|
|
|
|
And a method to create a `SecretQuestionCredentialModel` from the question and answer:
|
|
|
|
[source,java]
|
|
----
|
|
private SecretQuestionCredentialModel(String question, String answer) {
|
|
credentialData = new SecretQuestionCredentialData(question);
|
|
secretData = new SecretQuestionSecretData(answer);
|
|
}
|
|
|
|
public static SecretQuestionCredentialModel createSecretQuestion(String question, String answer) {
|
|
SecretQuestionCredentialModel credentialModel = new SecretQuestionCredentialModel(question, answer);
|
|
credentialModel.fillCredentialModelFields();
|
|
return credentialModel;
|
|
}
|
|
|
|
private void fillCredentialModelFields(){
|
|
try {
|
|
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
|
|
setSecretData(JsonSerialization.writeValueAsString(secretData));
|
|
setType(TYPE);
|
|
setCreatedDate(Time.currentTimeMillis());
|
|
} catch (IOException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
----
|
|
|
|
==== Implementing a CredentialProvider
|
|
|
|
As with all Providers, to allow {project_name} to generate the CredentialProvider, we require a CredentialProviderFactory. For this requirement we create
|
|
the SecretQuestionCredentialProviderFactory, whose `create` method will be called when a SecretQuestionCredentialProvider is asked for:
|
|
|
|
[source,java]
|
|
----
|
|
public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory<SecretQuestionCredentialProvider> {
|
|
|
|
public static final String PROVIDER_ID = "secret-question";
|
|
|
|
@Override
|
|
public String getId() {
|
|
return PROVIDER_ID;
|
|
}
|
|
|
|
@Override
|
|
public CredentialProvider create(KeycloakSession session) {
|
|
return new SecretQuestionCredentialProvider(session);
|
|
}
|
|
}
|
|
----
|
|
|
|
The CredentialProvider interface takes a generic parameter that extends a CredentialModel. In our case we to use the SecretQuestionCredentialModel we created:
|
|
|
|
[source,java]
|
|
----
|
|
public class SecretQuestionCredentialProvider implements CredentialProvider<SecretQuestionCredentialModel>, CredentialInputValidator {
|
|
private static final Logger logger = Logger.getLogger(SecretQuestionCredentialProvider.class);
|
|
|
|
protected KeycloakSession session;
|
|
|
|
public SecretQuestionCredentialProvider(KeycloakSession session) {
|
|
this.session = session;
|
|
}
|
|
|
|
----
|
|
|
|
We also want to implement the CredentialInputValidator interface, as this allows {project_name} to know that this provider can also be used to validate a
|
|
credential for an Authenticator. For the CredentialProvider interface, the first method that needs to be implemented is the `getType()` method. This will simply
|
|
return the `SecretQuestionCredentialModel`'s TYPE String:
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public String getType() {
|
|
return SecretQuestionCredentialModel.TYPE;
|
|
}
|
|
----
|
|
|
|
The second method is to create a `SecretQuestionCredentialModel` from a `CredentialModel`. For this method we simply call the existing static method
|
|
from `SecretQuestionCredentialModel`:
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public SecretQuestionCredentialModel getCredentialFromModel(CredentialModel model) {
|
|
return SecretQuestionCredentialModel.createFromCredentialModel(model);
|
|
}
|
|
----
|
|
|
|
Finally, we have the methods to create a credential and delete a credential. These methods call the UserModel's credential manager, which
|
|
is responsible for knowing where to read or write the credential, for example local storage or federated storage.
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public CredentialModel createCredential(RealmModel realm, UserModel user, SecretQuestionCredentialModel credentialModel) {
|
|
if (credentialModel.getCreatedDate() == null) {
|
|
credentialModel.setCreatedDate(Time.currentTimeMillis());
|
|
}
|
|
return user.credentialManager().createStoredCredential(credentialModel);
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {
|
|
return user.credentialManager().removeStoredCredentialById(credentialId);
|
|
}
|
|
----
|
|
|
|
For the CredentialInputValidator, the main method to implement is the `isValid`, which tests whether a credential is valid for a
|
|
given user in a given realm. This is the method that is called by the Authenticator when it seeks to validate the user's input. Here we
|
|
simply need to check that the input String is the one recorded in the Credential:
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
|
if (!(input instanceof UserCredentialModel)) {
|
|
logger.debug("Expected instance of UserCredentialModel for CredentialInput");
|
|
return false;
|
|
}
|
|
if (!input.getType().equals(getType())) {
|
|
return false;
|
|
}
|
|
String challengeResponse = input.getChallengeResponse();
|
|
if (challengeResponse == null) {
|
|
return false;
|
|
}
|
|
CredentialModel credentialModel = getCredentialStore().getStoredCredentialById(realm, user, input.getCredentialId());
|
|
SecretQuestionCredentialModel sqcm = getCredentialFromModel(credentialModel);
|
|
return sqcm.getSecretQuestionSecretData().getAnswer().equals(challengeResponse);
|
|
}
|
|
----
|
|
|
|
The other two methods to implement are a test if the CredentialProvider supports the given credential type and a test to check
|
|
if the credential type is configured for a given user. For our case, the latter test simply means checking if the user has a credential
|
|
of the SECRET_QUESTION type:
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public boolean supportsCredentialType(String credentialType) {
|
|
return getType().equals(credentialType);
|
|
}
|
|
|
|
@Override
|
|
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
|
if (!supportsCredentialType(credentialType)) return false;
|
|
return !getCredentialStore().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
|
|
}
|
|
----
|
|
|
|
==== Implementing an authenticator
|
|
|
|
When implementing an authenticator that uses Credentials to authenticate a user, you should have the authenticator implement
|
|
the CredentialValidator interface. This interfaces takes a class extending a CredentialProvider as a parameter, and will
|
|
allow {project_name} to directly call the methods from the CredentialProvider. The only method that needs to be implemented is
|
|
`getCredentialProvider` method, which in our example allows the SecretQuestionAuthenticator to retrieve the SecretQuestionCredentialProvider:
|
|
|
|
[source,java]
|
|
----
|
|
public SecretQuestionCredentialProvider getCredentialProvider(KeycloakSession session) {
|
|
return (SecretQuestionCredentialProvider)session.getProvider(CredentialProvider.class, SecretQuestionCredentialProviderFactory.PROVIDER_ID);
|
|
}
|
|
----
|
|
|
|
When implementing the Authenticator interface, the first method that needs to be implemented is the requiresUser() method.
|
|
For our example, this method must return true as we need to validate the secret question associated with the user.
|
|
A provider like kerberos would return false from this method as it can resolve a user from the negotiate header.
|
|
This example, however, is validating a specific credential of a specific user.
|
|
|
|
The next method to implement is the configuredFor() method.
|
|
This method is responsible for determining if the user is configured for this particular authenticator. In our case,
|
|
we can just call the method implemented in the SecretQuestionCredentialProvider
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
|
return getCredentialProvider(session).isConfiguredFor(realm, user, getType(session));
|
|
}
|
|
----
|
|
|
|
The next method to implement on the Authenticator is setRequiredActions(). If configuredFor() returns false and our example authenticator
|
|
is required within the flow, this method will be called, but only if the associated AuthenticatorFactory's `isUserSetupAllowed` method returns true.
|
|
The setRequiredActions() method is responsible for registering any required actions that must be performed by the user.
|
|
In our example, we need to register a required action that will force the user to set up the answer to the secret question.
|
|
We will implement this required action provider later in this chapter.
|
|
Here is the implementation of the setRequiredActions() method.
|
|
|
|
[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 = context.form()
|
|
.createForm("secret-question.ftl");
|
|
context.challenge(challenge);
|
|
}
|
|
|
|
protected boolean hasCookie(AuthenticationFlowContext context) {
|
|
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
|
|
boolean result = cookie != null;
|
|
if (result) {
|
|
System.out.println("Bypassing secret question because cookie is set");
|
|
}
|
|
return result;
|
|
}
|
|
----
|
|
|
|
The hasCookie() method checks to see if there is already a cookie set on the browser which indicates that the secret question has already been answered.
|
|
If that returns true, we just mark this execution's status as SUCCESS using the AuthenticationFlowContext.success() method and returning from the authentication() method.
|
|
|
|
If the hasCookie() method returns false, we must return a response that renders the secret question HTML form.
|
|
AuthenticationFlowContext has a form() method that initializes a Freemarker page builder with appropriate base information needed to build the form.
|
|
This page builder is called `org.keycloak.login.LoginFormsProvider`. The LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme.
|
|
Additionally you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the Freemarker template.
|
|
We'll go over this later.
|
|
|
|
Calling LoginFormsProvider.createForm() returns a JAX-RS Response object.
|
|
We then call AuthenticationFlowContext.challenge() passing in this response.
|
|
This sets the status of the execution as CHALLENGE and if the execution is Required, this JAX-RS Response object will be sent to the browser.
|
|
|
|
So, the HTML page asking for the answer to a secret question is displayed to the user and the user enters in the answer and clicks submit.
|
|
The action URL of the HTML form will send an HTTP request to the flow.
|
|
The flow will end up invoking the action() method of our Authenticator implementation.
|
|
|
|
[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 validation itself gets the data that was received from the form, and calls the isValid method from the SecretQuestionCredentialProvider. You'll notice
|
|
that there's a section of the code concerning getting the credential Id. This is because if {project_name} is configured to allow multiple types of alternative
|
|
authenticators, or if the user could record multiple credentials of the SECRET_QUESTION type (for example if we allowed to choose from several questions,
|
|
and we allowed the user to have answers for more than one of those questions), then {project_name} needs to know which credential is being used to log the user.
|
|
In case there is more than one credential, {project_name} allows the user to choose during the login which credential is being used, and the information is transmitted by
|
|
the form to the Authenticator.
|
|
In case the form doesn't present this information, credential id used is given by the CredentialProvider's `default getDefaultCredential` method, which will
|
|
return the "most preferred" credential of the correct type of the user,
|
|
|
|
[source,java]
|
|
----
|
|
protected boolean validateAnswer(AuthenticationFlowContext context) {
|
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
|
String secret = formData.getFirst("secret_answer");
|
|
String credentialId = formData.getFirst("credentialId");
|
|
if (credentialId == null || credentialId.isEmpty()) {
|
|
credentialId = getCredentialProvider(context.getSession())
|
|
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
|
|
}
|
|
|
|
UserCredentialModel input = new UserCredentialModel(credentialId, getType(context.getSession()), secret);
|
|
return getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input);
|
|
}
|
|
----
|
|
|
|
Next method is the setCookie().
|
|
This is an example of providing configuration for the Authenticator.
|
|
In this case we want the max age of the cookie to be configurable.
|
|
|
|
[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"));
|
|
|
|
}
|
|
URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build();
|
|
addCookie(context, "SECRET_QUESTION_ANSWERED", "true",
|
|
uri.getRawPath(),
|
|
null, null,
|
|
maxCookieAge,
|
|
false, true);
|
|
}
|
|
----
|
|
|
|
We obtain an AuthenticatorConfigModel from the AuthenticationFlowContext.getAuthenticatorConfig() method.
|
|
If configuration exists we pull the max age config out of it.
|
|
We will see how we can define what should be configured when we talk about the AuthenticatorFactory implementation.
|
|
The config values can be defined within the Admin Console if you set up config definitions in your AuthenticatorFactory implementation.
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {
|
|
return CredentialTypeMetadata.builder()
|
|
.type(getType())
|
|
.category(CredentialTypeMetadata.Category.TWO_FACTOR)
|
|
.displayName(SecretQuestionCredentialProviderFactory.PROVIDER_ID)
|
|
.helpText("secret-question-text")
|
|
.createAction(SecretQuestionAuthenticatorFactory.PROVIDER_ID)
|
|
.removeable(false)
|
|
.build(session);
|
|
}
|
|
----
|
|
|
|
The last method to implement in the SecretQuestionCredentialProvider class is getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext), which is an abstract method of the CredentialProvider interface. Each Credential provider has to provide and implement this method. The method returns an instance of CredentialTypeMetadata,
|
|
which should at least include type and category of authenticator, displayName and removable item. In this example, the builder
|
|
takes type of authenticator from method getType(), category is Two Factor (the authenticator can be used as second factor of authentication)
|
|
and removable, which is set up to false (user can't remove some previously registered credentials).
|
|
|
|
Other items of builder are helpText (will be shown to the user on various screens), createAction (the providerID of the required action,
|
|
which can be used by the user to create new credential) or updateAction (same as createAction, but instead of creating the new credential, it will update the credential).
|
|
|
|
==== Implementing an AuthenticatorFactory
|
|
|
|
The next step in this process is to implement an AuthenticatorFactory.
|
|
This factory is responsible for instantiating an Authenticator.
|
|
It also provides deployment and configuration metadata about the Authenticator.
|
|
|
|
The getId() method is just the unique name of the component.
|
|
The create() method is called by the runtime to allocate and process the Authenticator.
|
|
|
|
[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 to specify the allowed requirement switches.
|
|
While there are four different requirement types: ALTERNATIVE, REQUIRED, CONDITIONAL, DISABLED, AuthenticatorFactory implementations can limit which
|
|
requirement options are shown in the Admin Console when defining a flow. CONDITIONAL should only always be used for subflows, and unless there's a good
|
|
reason for doing otherwise, the requirement on an authenticator should be REQUIRED, ALTERNATIVE and DISABLED:
|
|
|
|
[source,java]
|
|
----
|
|
|
|
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
|
AuthenticationExecutionModel.Requirement.REQUIRED,
|
|
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
|
AuthenticationExecutionModel.Requirement.DISABLED
|
|
};
|
|
@Override
|
|
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
|
return REQUIREMENT_CHOICES;
|
|
}
|
|
----
|
|
|
|
The AuthenticatorFactory.isUserSetupAllowed() is a flag that tells the flow manager whether or not Authenticator.setRequiredActions() method will be called.
|
|
If an Authenticator is not configured for a user, the flow manager checks isUserSetupAllowed(). If it is false, then the flow aborts with an error.
|
|
If it returns true, then the flow manager will invoke Authenticator.setRequiredActions().
|
|
|
|
[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 the text that will be shown in the Admin Console when listing the Authenticator.
|
|
getReferenceCategory() is just a category the Authenticator belongs to.
|
|
|
|
==== Adding an authenticator form
|
|
|
|
{project_name} 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 added to the `theme-resources/templates` in your JAR, see <<_theme_resource,Theme Resource Provider>> for more details.
|
|
|
|
Let's take a bigger look at secret-question.ftl Here's a small code snippet:
|
|
|
|
[source,xml]
|
|
----
|
|
|
|
<form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
|
<div class="${properties.kcFormGroupClass!}">
|
|
<div class="${properties.kcLabelWrapperClass!}">
|
|
<label for="totp" class="${properties.kcLabelClass!}">${msg("loginSecretQuestion")}</label>
|
|
</div>
|
|
|
|
<div class="${properties.kcInputWrapperClass!}">
|
|
<input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
|
|
</div>
|
|
</div>
|
|
</form>
|
|
----
|
|
|
|
Any piece of text enclosed in `${}` corresponds to an attribute or template function.
|
|
If you see the form's action, you see it points to `${url.loginAction}`.
|
|
This value is automatically generated when you invoke the AuthenticationFlowContext.form() method.
|
|
You can also obtain this value by calling the AuthenticationFlowContext.getActionURL() method in Java code.
|
|
|
|
You'll also see `${properties.someValue}`.
|
|
These correspond to properties defined in your theme.properties file of our theme.
|
|
`${msg("someValue")}` corresponds to the internationalized message bundles (.properties files) included with the login theme messages/ directory.
|
|
If you're just using english, you can just add the value of the `loginSecretQuestion`.
|
|
This should be the question you want to ask the user.
|
|
|
|
When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvider instance.
|
|
If you called, `LoginFormsProvider.setAttribute("foo", "bar")`, the value of "foo" would be available for reference in your form as `${foo}`.
|
|
The value of an attribute can be any Java bean as well.
|
|
|
|
If you look at the top of the file, you'll see that we are importing a template:
|
|
|
|
[source,xml]
|
|
----
|
|
<#import "select.ftl" as layout>
|
|
----
|
|
|
|
Importing this template, instead of the standard `template.ftl` allows {project_name} to display a dropdown box that allows the user to select
|
|
a different credential or execution.
|
|
|
|
[[_adding_authenticator]]
|
|
==== Adding an authenticator to a flow
|
|
|
|
Adding an Authenticator to a flow must be done in the Admin Console.
|
|
If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently defined flows.
|
|
You cannot modify built in flows, so, to add the Authenticator we've created you have to copy an existing flow or create your own.
|
|
Our hope is that the user interface is sufficiently clear so that you can determine how to create a flow and add the Authenticator. For
|
|
more details, see the `Authentication Flows` chapter in link:{adminguide_link}[{adminguide_name}] .
|
|
|
|
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 {project_name} to scan the providers it has to load into the system.
|
|
|
|
To deploy this jar, copy it to the `providers/` directory, then run `bin/kc.[sh|bat] build`.
|
|
|
|
==== Implement the RequiredActionProvider
|
|
|
|
Required actions must first implement the RequiredActionProvider interface.
|
|
The RequiredActionProvider.requiredActionChallenge() is the initial call by the flow manager into the required action.
|
|
This method is responsible for rendering the HTML form that will drive the required action.
|
|
|
|
[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 action 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 or extending the registration form
|
|
|
|
It is entirely possible for you to implement your own flow with a set of Authenticators to totally change how registration is done in {project_name}.
|
|
But what you'll usually want to do is just add a bit of validation to the out-of-the-box registration page.
|
|
An additional SPI was created to be able to do this.
|
|
It basically allows you to add validation of form elements on the page as well as to initialize UserModel attributes and data after the user has been registered.
|
|
We'll look at both the implementation of the user profile registration processing as well as the registration Google reCAPTCHA Enterprise plugin.
|
|
|
|
==== Implementation FormAction interface
|
|
|
|
The core interface you have to implement is the FormAction interface.
|
|
A FormAction is responsible for rendering and processing a portion of the page.
|
|
Rendering is done in the buildPage() method, validation is done in the validate() method, post validation operations are done in success(). Let's first take a look at buildPage() method of the Recaptcha plugin.
|
|
|
|
[source,java]
|
|
----
|
|
|
|
@Override
|
|
public void buildPage(FormContext context, LoginFormsProvider form) {
|
|
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
|
|
if (config == null
|
|
|| Stream.of(PROJECT_ID, SITE_KEY, API_KEY, ACTION)
|
|
.anyMatch(key -> Strings.isNullOrEmpty(config.get(key)))
|
|
|| parseDoubleFromConfig(config, SCORE_THRESHOLD) == null) {
|
|
form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
|
|
return;
|
|
}
|
|
|
|
String userLanguageTag = context.getSession().getContext().resolveLocale(context.getUser())
|
|
.toLanguageTag();
|
|
boolean invisible = Boolean.parseBoolean(config.getOrDefault(INVISIBLE, "true"));
|
|
|
|
form.setAttribute("recaptchaRequired", true);
|
|
form.setAttribute("recaptchaSiteKey", config.get(SITE_KEY));
|
|
form.setAttribute("recaptchaAction", config.get(ACTION));
|
|
form.setAttribute("recaptchaVisible", !invisible);
|
|
form.addScript("https://www.google.com/recaptcha/enterprise.js?hl=" + userLanguageTag);
|
|
}
|
|
----
|
|
|
|
The Recaptcha buildPage() method is a callback by the form flow to help render the page.
|
|
It receives a form parameter which is a LoginFormsProvider.
|
|
You can add additional attributes to the form provider so that they can be displayed in the HTML page generated by the registration Freemarker template.
|
|
|
|
The code above is from the registration recaptcha plugin.
|
|
Recaptcha requires some specific settings that must be obtained from configuration.
|
|
FormActions are configured in the exact same as Authenticators are.
|
|
In this example, we pull the Google Recaptcha site key and other options from Recaptcha configuration and add them as attributes to the form provider.
|
|
Our registration template file, register.ftl, can now have access to those attributes.
|
|
|
|
Recaptcha also has the requirement of loading a JavaScript script.
|
|
You can do this by calling LoginFormsProvider.addScript(), passing in the URL.
|
|
|
|
For user profile processing, there is no additional information that it needs to add to the form, so its buildPage() method is empty.
|
|
|
|
The next meaty part of this interface is the validate() method.
|
|
This is called immediately upon receiving a form post.
|
|
Let's look at the Recaptcha's plugin first.
|
|
|
|
[source,java]
|
|
----
|
|
|
|
@Override
|
|
public void validate(ValidationContext context) {
|
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
|
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
|
|
|
|
if (!Validation.isBlank(captcha) && validateRecaptcha(context, captcha)) {
|
|
context.success();
|
|
} else {
|
|
List<FormMessage> errors = new ArrayList<>();
|
|
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
|
|
formData.remove(G_RECAPTCHA_RESPONSE);
|
|
context.validationError(formData, errors);
|
|
}
|
|
}
|
|
|
|
----
|
|
|
|
Here we obtain the form data that the Recaptcha widget adds to the form.
|
|
We obtain the Recaptcha secret key from configuration.
|
|
We then validate the recaptcha.
|
|
If successful, ValidationContext.success() is called.
|
|
We clear the captcha token from the form using formData.remove, but keep other form data untouched.
|
|
If not, we invoke ValidationContext.validationError() passing in the formData (so the user doesn't have to re-enter data), we also specify an error message we want displayed.
|
|
The error message must point to a message bundle property in the internationalized message bundles.
|
|
For other registration extensions validate() might be validating the format of a form element, for example an alternative email attribute.
|
|
|
|
Let's also look at the user profile plugin that is used to validate email address and other user information when registering.
|
|
|
|
[source,java]
|
|
----
|
|
|
|
@Override
|
|
public void validate(ValidationContext context) {
|
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
|
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
|
|
|
UserProfile profile = getOrCreateUserProfile(context, formData);
|
|
|
|
try {
|
|
profile.validate();
|
|
} catch (ValidationException pve) {
|
|
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
|
|
|
if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
|
|
context.getEvent().detail(Details.EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL));
|
|
}
|
|
|
|
if (pve.hasError(Messages.EMAIL_EXISTS)) {
|
|
context.error(Errors.EMAIL_IN_USE);
|
|
} else if (pve.hasError(Messages.USERNAME_EXISTS)) {
|
|
context.error(Errors.USERNAME_IN_USE);
|
|
} else {
|
|
context.error(Errors.INVALID_REGISTRATION);
|
|
}
|
|
|
|
context.validationError(formData, errors);
|
|
return;
|
|
}
|
|
context.success();
|
|
}
|
|
----
|
|
|
|
As you can see, this validate() method of user profile processing makes sure that the email and all other attributes are filled in the form.
|
|
It delegates to User Profile SPI, which makes sure that email is in the right format and does all other validations.
|
|
If any of these validations fail, an error message is queued up for rendering. It would contain the message for every field where the validation failed.
|
|
|
|
NOTE: As you can see, the user profile makes sure that registration form contains all the needed user profile fields. User profile also makes sure that correct validations
|
|
are used, attributes are correctly grouped on the page. There is a correct type used for each field (such as if a user needs to choose from predefined values), fields
|
|
are "conditionally" rendered just for some scopes (Progressive profiling) and others. So usually you will not need to implement new `FormAction` or registration fields, but
|
|
you can just properly configure user-profile to reflect this. For more details, see link:{adminguide_link}#user-profile[User Profile documentation].
|
|
In general, new FormAction might be useful for instance if you want to add new credentials to the registration form (such as ReCaptcha support as mentioned here) rather than new user profile fields.
|
|
|
|
After all validations have been processed then, the form flow then invokes the FormAction.success() method.
|
|
For recaptcha this is a no-op, so we won't go over it.
|
|
For user profile processing, this method fills in values in the registered user.
|
|
|
|
[source,java]
|
|
----
|
|
|
|
@Override
|
|
public void success(FormContext context) {
|
|
checkNotOtherUserAuthenticating(context);
|
|
|
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
|
|
|
String email = formData.getFirst(UserModel.EMAIL);
|
|
String username = formData.getFirst(UserModel.USERNAME);
|
|
|
|
if (context.getRealm().isRegistrationEmailAsUsername()) {
|
|
username = email;
|
|
}
|
|
|
|
context.getEvent().detail(Details.USERNAME, username)
|
|
.detail(Details.REGISTER_METHOD, "form")
|
|
.detail(Details.EMAIL, email);
|
|
|
|
UserProfile profile = getOrCreateUserProfile(context, formData);
|
|
UserModel user = profile.create();
|
|
|
|
user.setEnabled(true);
|
|
|
|
// This means that following actions can retrieve user from the context by context.getUser() method
|
|
context.setUser(user);
|
|
}
|
|
----
|
|
|
|
The new user is created and the UserModel of the newly registered user is added to the FormContext.
|
|
The appropriate methods are called to initialize UserModel data. In your own FormAction, you can possibly obtain user by using something like:
|
|
[source,java]
|
|
----
|
|
|
|
@Override
|
|
public void success(FormContext context) {
|
|
UserModel user = context.getUser();
|
|
if (user != null) {
|
|
// Do something useful with the user here ...
|
|
}
|
|
}
|
|
----
|
|
|
|
Finally, you are also required to define a FormActionFactory class.
|
|
This class is implemented similarly to AuthenticatorFactory, so we won't go over it.
|
|
|
|
==== Packaging the action
|
|
|
|
You will package your classes within a single jar.
|
|
This jar must contain a file named `org.keycloak.authentication.FormActionFactory` and must be contained in the `META-INF/services/` directory of your jar.
|
|
This file must list the fully qualified class name of each FormActionFactory implementation you have in the jar.
|
|
For example:
|
|
|
|
[source]
|
|
----
|
|
|
|
org.keycloak.authentication.forms.RegistrationUserCreation
|
|
org.keycloak.authentication.forms.RegistrationRecaptcha
|
|
----
|
|
|
|
This services/ file is used by {project_name} to scan the providers it has to load into the system.
|
|
|
|
To deploy this jar, copy it to the `providers/` directory, then run `bin/kc.[sh|bat] build`.
|
|
|
|
==== Adding FormAction to the registration flow
|
|
|
|
Adding a FormAction to a registration page flow must be done in the Admin Console.
|
|
If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently defined flows.
|
|
You cannot modify built in flows, so, to add the Authenticator we've created you have to copy an existing flow or create your own.
|
|
I'm hoping the UI is intuitive enough so that you can figure out for yourself how to create a flow and add the FormAction.
|
|
|
|
Basically you'll have to copy the registration flow.
|
|
Then click Actions menu to the right of the Registration Form, and pick "Add execution" to add a new execution.
|
|
You'll pick the FormAction from the selection list.
|
|
Make sure your FormAction comes after "Registration User Creation" by using the down buttons to move it if your FormAction isn't already listed after "Registration User Creation". You want your FormAction to come after user creation because the success() method of Registration User Creation is responsible for creating the new UserModel.
|
|
|
|
After you've created your flow, you have to bind it to registration.
|
|
If you go to the Authentication menu and go to the Bindings tab you will see options to bind a flow to the browser, registration, or direct grant flow.
|
|
|
|
=== Modifying forgot password/credential flow
|
|
|
|
{project_name} 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, {project_name} asks for the email or username of the user and sends an email to them.
|
|
If the user clicks on the link, then they are able to reset both their password and OTP (if an OTP has been set up). You can disable automatic OTP reset by disabling the "Reset OTP" authenticator in the flow.
|
|
|
|
You can add additional functionality to this flow as well.
|
|
For example, many deployments would like for the user to answer one or more secret questions in additional to sending an email with a link.
|
|
You could expand on the secret question example that comes with the distro and incorporate it into the reset credential flow.
|
|
|
|
One thing to note if you are extending the reset credentials flow.
|
|
The first "authenticator" is just a page to obtain the username or email.
|
|
If the username or email exists, then the AuthenticationFlowContext.getUser() will return the located user.
|
|
Otherwise this will be null.
|
|
This form *WILL NOT* re-ask the user to enter an email or username if the previous email or username did not exist.
|
|
You need to prevent attackers from being able to guess valid users.
|
|
So, if AuthenticationFlowContext.getUser() returns null, you should proceed with the flow to make it look like a valid user was selected.
|
|
I suggest that if you want to add secret questions to this flow, you should ask these questions after the email is sent.
|
|
In other words, add your custom authenticator after the "Send Reset Email" authenticator.
|
|
|
|
=== Modifying first broker login flow
|
|
|
|
First Broker Login flow is used during first login with some identity provider.
|
|
Term `First Login` means that there is not yet existing {project_name} account linked with the particular authenticated identity provider account.
|
|
|
|
[role="_additional-resource"]
|
|
.Additional resources
|
|
* See the `Identity Brokering` chapter in link:{adminguide_link}[{adminguide_name}] .
|
|
|
|
[[_client_authentication]]
|
|
=== Authentication of clients
|
|
|
|
{project_name} actually supports pluggable authentication for https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect] client applications.
|
|
Authentication of client (application) is used under the hood by the {project_name} adapter during sending any backchannel requests
|
|
to the {project_name} server (like the request for exchange code to access token after successful authentication or request to refresh token).
|
|
But the client authentication can be also used directly by you during `Direct Access grants` (represented by OAuth2 `Resource Owner Password Credentials Flow`)
|
|
or during `Service account` authentication (represented by OAuth2 `Client Credentials Flow`).
|
|
|
|
[role="_additional-resource"]
|
|
.Additional resources
|
|
* For more details about {project_name} adapter and OAuth2 flows see link:{adapterguide_link}[{adapterguide_name}].
|
|
|
|
==== Default implementations
|
|
|
|
Actually {project_name} has 2 default implementations of client authentication:
|
|
|
|
Traditional authentication with client_id and client_secret::
|
|
This is default mechanism mentioned in the https://openid.net/specs/openid-connect-core-1_0.html[OpenID Connect] or https://datatracker.ietf.org/doc/html/rfc6749[OAuth2] specification and {project_name} supports it since it's early days.
|
|
The public client needs to include `client_id` parameter with its ID in the POST request (so it's de facto not authenticated) and the confidential client needs to include `Authorization: Basic` header with the clientId and clientSecret used as username and password.
|
|
|
|
Authentication with signed JWT::
|
|
This is based on the https://datatracker.ietf.org/doc/html/rfc7523[JWT Bearer Token Profiles for OAuth 2.0] specification.
|
|
The client/adapter generates the https://datatracker.ietf.org/doc/html/rfc7519[JWT] and signs it with his private key.
|
|
The {project_name} then verifies the signed JWT with the client's public key and authenticates client based on it.
|
|
|
|
See the demo example and especially the `examples/preconfigured-demo/product-app` for the example application showing
|
|
the application using client authentication with signed JWT.
|
|
|
|
==== Implement your own client authenticator
|
|
|
|
For plug your own client authenticator, you need to implement few interfaces on both client (adapter) and server side.
|
|
|
|
Client side::
|
|
Here you need to implement `org.keycloak.adapters.authentication.ClientCredentialsProvider` and put the implementation either to:
|
|
|
|
* your WAR file into WEB-INF/classes . But in this case, the implementation can be used just for this single WAR application
|
|
* Some JAR file, which will be added into WEB-INF/lib of your WAR
|
|
* Some JAR file, which will be used as jboss module and configured in jboss-deployment-structure.xml of your WAR. In all cases, you also need to create the file `META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider` either in the WAR or in your JAR.
|
|
|
|
Server side::
|
|
Here you need to implement `org.keycloak.authentication.ClientAuthenticatorFactory` 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.
|