KEYCLOAK-11745 Multi-factor authentication documentation

Co-authored-by: rpo <harture414@gmail.com>
Co-authored-by: mposolda <mposolda@gmail.com>
This commit is contained in:
AlistairDoswald 2019-11-12 17:32:33 +01:00 committed by Marek Posolda
parent a3e946e7cb
commit a1d70c252e
21 changed files with 903 additions and 315 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -45,6 +45,18 @@ In this release, we added initial support for W3C Web Authentication (WebAuthn).
however we are working on further improvements in this area. Thanks to https://github.com/tnorimat[tnorimat] for the contribution. Also thanks to
https://github.com/ynojima[ynojima] for the help and feedback.
== Support for password-less authentication, multi-factor authentication and multiple credentials per user
With the arrival of W3C Web Authentication support, we've refined the authentication flow system to be able to allow a user to select which authentication method he
wishes to use during his login (for example, to choose between his OTP credential and his WebAuthn credential). The new mechanisms also allow an administrator to
craft flows for password-less login, for example just using WebAuthn as an authentication method. Please note that with these changes, any custom authentication
flow you have created may need to be adapted to the new flow logic.
As a result of these changes, it is now also possible for users to have multiple OTP devices and multiple WebAuthn devices. The same system that allows a user
to select which type of device to use during login also allows him to select which specific device to use. Thanks to the https://github.com/cloudtrust[Cloudtrust] team:
https://github.com/AlistairDoswald[AlistairDoswald], https://github.com/fperot74[sispeo] and https://github.com/Fratt[Fratt] for their contributions, and
to https://github.com/harture[harture] and https://github.com/lagess[Laurent] for their help.
= Other Improvements

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View file

@ -5,7 +5,14 @@
An _authentication flow_ is a container for all authentications, screens, and actions that must happen during login, registration, and other
{project_name} workflows.
If you go to the admin console `Authentication` left menu item and go to the `Flows` tab, you can view all the defined flows
in the system and what actions and checks each flow requires. This section does a walk-through of the browser login flow. In the
in the system and what actions and checks each flow requires.
==== Built-in flows
{project_name} comes with a certain number of built-in flows. These flows cannot be modified, but the requirements can be modified to
suit your needs.
This section does a walk-through of the built-in browser login flow. In the
left drop-down list select `browser` to come to the screen shown below:
.Browser Flow
@ -15,36 +22,182 @@ If you hover over the tooltip (the tiny question mark) to the right of the flow
flow is and does.
The `Auth Type` column is the name of authentication or action that will be executed. If an authentication is indented
this means it is in a sub-flow and may or may not be executed depending on the behavior of its parent. The `Requirement`
column is a set of radio buttons which define whether or not the action will execute. Let's describe what each radio
button means:
this means it is in a sub-flow and may or may not be executed depending on the behavior of its parent. The `Requirement`
column is a set of radio buttons which define whether or not the action will execute. Let's describe what each radio
button means in this context:
Required::
This authentication execution must execute successfully. If the user doesn't have that type of authentication mechanism
This authentication execution must execute successfully. If the user doesn't have that type of authentication mechanism
configured and there is a required action associated with that authentication type, then a required action will be attached
to that account. For example, if you switch `OTP Form` to `Required`, users that don't have an OTP generator configured
to that account. For example, if you switch the `Browser - Conditional OTP` sub-flow to `Required`, users that don't have an OTP generator configured
will be asked to do so.
Optional::
If the user has the authentication type configured, it will be executed. Otherwise, it will be ignored.
Disabled::
If disabled, the authentication type is not executed.
Alternative::
This means that at least one alternative authentication type must execute successfully at that level of the flow.
Disabled::
If disabled, the authentication type is not executed.
Conditional::
This requirement type can only be set on sub-flows, indicating that it will only be executed if its condition evaluates to true.
This is better described in an example. Let's walk through the `browser` authentication flow.
. The first authentication type is `Cookie`. When a user successfully logs in for the first time, a session cookie is set.
If this cookie has already been set, then this authentication type is successful.
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.
. Next the flow looks at the Kerberos execution. This authenticator is disabled by default and will be skipped.
. The next execution is a subflow called Forms. Since this subflow is marked as _alternative_ it will not be executed if the `Cookie` authentication type passed.
This subflow contains additional authentication type that needs to be executed.
The executions for this subflow are loaded and the same processing logic occurs
. The first execution in the Forms subflow is the Username Password Form. This authentication type renders the username and password page.
If this cookie has already been set, then this authentication type is successful. In this case,
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.
. The second execution of the flow looks at the `Kerberos` execution. This authenticator is disabled by default and will be skipped.
. The third execution is the `Identity Provider Redirector`. It can be configured through the `Actions` > `Config` link to automatically redirect to another IdP for <<_identity_broker, identity brokering>>.
. The next execution is a sub-flow called `Forms`. Since this sub-flow is marked as _alternative_ it will not be executed if the `Cookie` authentication type passed.
This sub-flow contains additional authentication type that needs to be executed.
The executions for this sub-flow are loaded and the same processing logic occurs.
. The first execution in the Forms sub-flow is the Username Password Form. This authentication type renders the username and password page.
It is marked as _required_ so the user must enter in a valid username and password.
. The next execution is the OTP Form.
This is marked as _optional_. If the user has OTP set up, then this authentication type must run and be successful. If the user doesn't
have OTP set up, this authentication type is ignored.
. The second execution in the Forms sub-flow is a new sub-flow: the `Browser - Conditional OTP` sub-flow. Since this sub-flow is _conditional_, whether it is executed depends on the result of the
evaluation of the `Condition - User Configured` execution. If it is, the executions for this sub-flow are loaded and the same processing logic occurs
. The next execution is the `Condition - User Configured`. This checks if the other executions in the flow are configured for the user.
Meaning that the `Browser - Conditional OTP` sub-flow will only be executed if the user has an OTP credential configured.
. The final execution is the `OTP Form`. This is marked as _required_, but because of the setup in the _conditional_ subflow, it will only be run if the user
has an OTP credential set up. If he doesn't, the user will not see an OTP form.
==== Creating flows
This section explains in greater depth how flows work, and how to create your own flows. Note that there are important functionality and security
considerations when designing your own flow. A badly created flow could either let no one log in, let users in with less verification than you would
like, or simply result in an error.
To create a flow, you can either:
. Copy and then modify an existing flow. To do this select an existing flow (for example the `Browser` flow), and press the `Copy` button.
This will then ask you to set a name for the new flow, before creating it.
. Create a new flow from scratch. To do this press the `New` button. Since this is the more general case, we will use this for our example.
When creating a new flow, you will have to create a top level flow
.Create a top level flow
image:{project_images}/Create-top-level-flow.png[]
With the following options:
Alias::
The name of the flow.
Description::
The description you can set to the flow.
Top Level Flow Type::
The type of flow. the type `client` is used only for the authentication of clients (applications). For all other cases choose `generic`.
Once the flow is created, in addition to the `New` and `Copy` buttons, you now have, `Delete`, `Add execution` and `Add flow`.
.An empty new flow
image:{project_images}/New-flow.png[]
What a flow finally does is determined by the structure of the flow and sub-flows, the executions in those flows, and the requirements set on the
sub-flows and the executions.
Executions can be added with the `Add execution` button. Executions can have a wide variety of actions, from sending a reset email to validating an OTP. If you hover over the
tooltip (the tiny question mark) next to `Provider`, this will describe what the execution does.
.Adding an authentication execution
image:{project_images}/Create-authentication-execution.png[]
These can be divided into _automatic executions_ and _interactive executions_. _Automatic executions_ are similar to the `Cookie` execution, and will automatically
perform their action when they are encountered in the flow. _Interactive executions_ will halt the flow, usually to get some user input. Executions that execute
successfully will get the _success_ status. This is important, because this is part of whether a flow is successful or not. For example, an empty `Browser` flow
would not allow anyone to log in. For that it would need at least one execution that successfully evaluates, for example a `Username Password Form` that is corrected
filled and submitted.
Sub-flows can be added in top level flow with the `Add flow` button, which opens a `Create Execution Flow` page that is very similar to the `Create Top Level Form`
page. The only difference is that the `Flow Type` can be either `generic` (like before), or `form`. The `form` type is used to construct a sub-flow that generates
a single form for the user, like what is done for the built-in `Registration` flow. Sub-flows are a special type of execution that evaluate as successful
depending on how the executions they contain evaluate (and this includes the evaluation of their contained sub-flows). And the logic of this evaluation
depends on the Requirement of each execution and sub-flow.
Fully understanding this requires a more complete explanation of how requirements work when evaluating a flow, and this also applies to sub-flows:
Required::
For a flow to be evaluated as successful, all required elements in the flow must evaluate as successful. This means that all _Required_ elements in the flow
must be sequentially executed, from top to bottom, unless one the the elements causes the flow to fail. However, this is only true for the current flow.
Any _Required_ element within a sub-flow is only processed if that sub-flow is entered.
Alternative::
When a flow contains only _Alternative_ elements, only a single element must evaluate as successful for the flow to evaluate as successful.
Because the _Required_ flow elements within a flow are sufficient to mark a flow as successful, any _Alternative_ flow element within a flow
that contains _Required_ flow elements will never be executed. In this case, they are functionally _Disabled_.
Disabled::
Any _Disabled_ element is not evaluated and does not count to mark a flow as successful.
Conditional::
This requirement type can only be set on sub-flows. A _Conditional_ sub-flow can contain a "Condition" execution. These "Condition" executions must evaluate as
logical statements. If all "Condition" executions evaluate as _true_ then the _Conditional_ sub-flow acts as _Required_. If not, the _Conditional_ sub-flow
acts as _Disabled_. If no "Condition" execution is set, the _Conditional_ sub-flow acts as _Disabled_. If a flow contains "Condition" executions and is not set to
_Conditional_, the "Condition" executions are not evaluated, and can be considered functionally _Disabled_.
Note that after adding an execution, you should check that the Requirement is set to the correct value. Even if there is only a single possible Requirement, it
can happen that it is not set.
When constructing a flow, all elements added to the flow will have an `Actions` menu on the right-hand side. All elements added to the flow have a `Delete`
option in this menu to remove it from the flow. Executions can contain a `Config` menu option to configure the execution, as is the case for the
`Identity Provider Redirector`. Sub-flows can also have executions and sub-flows added to them, with their `Add execution` and `Add flow` menu options.
Finally, since the order of execution is important, you can move executions and sub-flows up and down within their respective flows with the up and down buttons
that are set to left of their name.
==== Creating a password-less browser login flow
To illustrate the creation of flows, this section describes the creation of a more advanced browser login flow. The purpose of this flow is to allow a
user to choose between logging in in a password-less manner using <<_webauthn, WebAuthn>>, and a two-factor authentication with password and OTP.
The flow to create is similar to the standard browser login, but diverges when reaching the username selection. Instead of copying the flow however, you'll be
creating the flow from the start:
* Select a realm, click on Authentication link
* Select "new", and give the new flow a distinctive Alias, i.e. "Browser Password-less"
* Select "Add execution", and using the drop-down select "Cookie". After pressing "Save", set its Requirement to _Alternative_.
* Select "Add execution", and using the drop-down select "Kerberos".
* Select "Add execution", and using the drop-down select "Identity Provider Redirector". After pressing "Save", set its Requirement to _Alternative_.
* Select "Add flow", and choose an representative Alias, e.g. "Forms". After pressing "Save", set its Requirement to _Alternative_.
.The common part with the browser flow
image:images/Passwordless-browser-login-common.png[]
* Using the `Actions` menu on the right-hand side of the "Forms" subflow, select "Add execution". Using the drop-down select
"Username Form". After pressing "Save", set its Requirement to _Required_.
The Username form is similar to "Browser" flow's Username Password Form, but only asks for a username, allowing a user to do perform a password-less login.
However, note that this inevitably allows a user enumeration attack on your {project_name} server. This is an unavoidable security risk for the convenience,
so the flow should make sure that an attacker cannot just have to guess a password to be able to enter.
* Using the `Actions` menu on the right-hand side of the "Forms" subflow, select "Add flow". Choose an representative Alias, e.g. "Authentication".
After pressing "Save", set its Requirement to _Required_.
* Using the `Actions` menu on the right-hand side of the "Authentication" subflow, select "Add execution". Using the drop-down select
"Webauthn Authenticator". After pressing "Save", set its Requirement to _Alternative_.
* Using the `Actions` menu on the right-hand side of the "Authentication" subflow, select "Add flow". Choose an representative Alias, e.g. "Password with OTP".
After pressing "Save", set its Requirement to _Alternative_.
* Using the `Actions` menu on the right-hand side of the "Password with OTP" subflow, select "Add execution". Using the drop-down select
"Password Form". After pressing "Save", set its Requirement to Required.
* Using the `Actions` menu on the right-hand side of the "Password with OTP" subflow, select "Add execution". Using the drop-down select
"OTP Form". After pressing "Save", set its Requirement to Required.
* In the "Bindings" menu, change the browser flow from "Browser" to "Browser Password-less"
The final flow that is produced is the following:
.A password-less browser login
image:images/Passwordless-browser-login.png[]
After entering the username, the way this flow works is the following:
* If the user has any WebAuthn credentials recorded, he will be able to use any of them to log in directly. This is the password-less login.
The user also has a drop-down allowing him to select "Password with OTP". He can do this because the "WebAuthn" execution and the "Password with OTP"
flow are set to _Alternative_. Were they set to _Required_ the user would have to enter WebAuthn, password, and OTP.
* If the user selects the "Password with OTP" from the drop-down, or if the user doesn't have any WebAuthn credentials, he will have to first enter his
password, and then his OTP. If the user has no OTP credential, he will be asked to record one.
It is important to note that since the WebAuthn execution is set to _Alternative_ instead of _Required_, this flow will never ask the user to register a WebAuthn credential. For a user
to have a Webauthn credential, he must have a required action added to him by an administrator. This is done first by making sure that that the `Webauthn Register`
required action is enabled in the realm (see the <<_webauthn,WebAuthn>> documentation), and then by setting the required action by using the `Credential Reset` part of a
user's <<user-credentials,Credentials>> management menu.
Creating a more advanced flow such as this one can have some subtle side effects. For example, if you were to enable the ability to reset the password
for the user, then this would be accessible from the password form. In the default "Reset Credentials" flow, the user has to enter his username. Since
he's already entered his username earlier in the "Browser Password-less" flow, this would be unnecessary for {project_name}, and a sub-optimal in terms of user
experience. To correct this, you could:
* Copy the "Reset Credentials" flow, setting its name to, for example "Reset Credentials for password-less"
* Use the `Actions` menu on the right-hand side of the "Choose user" execution, select "Delete"
* In the "Bindings" menu, change the reset credential flow from "Reset Credentials" to "Reset Credentials for password-less"
ifeval::[{project_community}==true]
=== Executions

View file

@ -8,24 +8,20 @@ IMPORTANT: Please note that WebAuthn support is still in development and not yet
NOTE: Whether WebAuthn's operations succeed depends on a user's WebAuthn supporting authenticator, browser and platform. If you use this WebAuthn support, please clarify to what extent those entities support the WebAuthn specification.
The major restriction is as follows :
- A user can register only one WebAuthn supporting Authenticator (called WebAuthn authenticator here).
- On registering the WebAuthn authenticator, its https://www.w3.org/TR/webauthn/#attestation-statement[Attestation Statement] is not verified.
- Only the Two-Factor Authentication (2FA) scenario is supported.
Also note that on registering the WebAuthn authenticator, its https://www.w3.org/TR/webauthn/#attestation-statement[Attestation Statement] is not verified.
==== Setup
The setup procedure of WebAuthn support for 2FA is the following :
Enable User Registration::
===== Enable User Registration
An administrator carries out the following operations on the `Admin Console` :
- Open the `Realm Settings -> Login` tab.
- Set the `User Registration` to ON and click `Save`.
Enable Webauthn Authenticator Registration::
===== Enable Webauthn Authenticator Registration
An administrator carries out the following operations on the `Admin Console` :
@ -34,107 +30,67 @@ An administrator carries out the following operations on the `Admin Console` :
- Select `Webauthn Register` as `Required Action`.
- Mark `Enabled` and `Default Action` checkbox.
Enable 2FA by Webauthn Authenticator::
===== Adding WebAuthn Authentication to a Browser Flow
An administrator carries out the following operations on the `Admin Console` :
* Select a realm, click on Authentication link, select the "Browser" flow
* Make a copy of the built-in "Browser" flow. You may want to give the new flow a distinctive name, i.e. "WebAuthn Browser"
* Using the drop down, select the copied flow
* Delete the `WebAuthn Browser Browser - Conditional OTP` sub-flow using its `Actions` menu
- Open the `Authentication -> Flows` tab.
- Copy and create the new browser flow.
- Add the `WebAuthn Authenticator` execution below the `User Password Form` execution.
- Set `REQUIRED` to the `WebAuthn Authenticator` execution's requirement.
- Open the `Authentication -> Bindings` tab.
- Select the created new browser flow as `Browser Flow`.
- Click `Save`.
If you want to have WebAuthn required for all users:
==== Register WebAuthn Authenticator
* Using the `Actions` menu of the `WebAuthn Browser Forms`, click on `Add execution`
* Select `WebAuthn Authenticator` using the drop down and click on "Save"
* Set its Requirement to _Required_.
The appropriate method to register a WebAuthen authenticator depends on if the user has or has not already registered an account on {project_name}.
image:images/webauthn-browser-flow-required.png[]
User without their account::
Note that in this scenario, if a user doesn't have a WebAuthn credential, a required action will be set that forces him
to register one.
A user carries out the following operations :
Alternatively, you can have users login with WebAuthn only if they have a WebAuthn credential registered, so instead of adding
the `WebAuthn Authenticator` execution:
- Open the login form.
- Click the `Register` link.
- Fill in items on the register form and click `Register`.
- The user's browser asks the user to register their WebAuthn authenticator.
- After successful registration, the user's browser asks the user to enter the text as their just registered WebAuthn authenticator's label.
* Using the `Actions` menu of the `WebAuthn Browser Forms`, click on `Add flow`
* Set the alias to "Conditional 2FA"
* Set the Requirement of `Conditional 2FA` to _Conditional_
* Using the `Actions` menu of the `Conditional 2FA`, click on `Add execution`
* Select `Condition - User Configured` using the drop down and click on "Save"
* Using the `Actions` menu of the `Conditional 2FA`, click on `Add execution`
* Select `WebAuthn Authenticator` using the drop down and click on "Save"
* Set its Requirement to _Alternative_.
User with their account::
image:images/webauthn-browser-flow-conditional.png[]
When such the users try to log in, they are required to register their WebAuthn authenticator automatically :
You can also allow the user to choose between using WebAuthn and OTP for his second factor:
- Open the login form.
- Fill in items, click `Save` and click `Login`.
- When the users log in, they are required to register their WebAuthn authenticator.
- After successful registration, the user's browser asks the user to enter the text as their just registered WebAuthn authenticator's label.
* Using the `Actions` menu of the `Conditional 2FA`, click on `Add execution`
* Select `OTP Form` using the drop down and click on "Save"
* Set its Requirement to _Alternative_.
==== Authenticate by WebAuthn Authenticator
image:images/webauthn-browser-flow-conditional-with-OTP.png[]
==== Authenticate with WebAuthn Authenticator
After registering their WebAuthn authenticator, the user carries out the following operations :
- Open the login form.
- Fill in items, click `Save` and click `Login`.
- Select the credential or credentials that {project_name} must accept for login. Then click `Save` and click `Login`.
- The list of registered Webauthn Authenticators' labels appears. Click `Authenticate`.
- The user's browser asks the user to authenticate by their WebAuthn authenticator.
- The user's browser asks the user to authenticate by their WebAuthn authenticator
==== View Registered WebAuthn Authenticator
==== Managing WebAuthn as an administrator
An administrator and a user can view the following information :
===== Managing Credentials
- https://www.w3.org/TR/webauthn/#credential-id[Credential ID]
- Label (WebAuthn authenticator's label the user entered on registering it)
- AAGUID
WebAuthn credentials are managed in a similar manner as other credentials, such as OTP, from the <<user-credentials, User credential management>>:
An administrator carries out the following operations on the `Admin Console` :
* Users can be assigned a required action to create a WebAuthn credential from the `Reset Actions` list, and selecting `Webauthn Register`
* The administrator can delete a WebAuthn credential by pressing it's `Delete` button
* The administrator can view the credential's data such as the AAGUID by selecting `Show data...`
* The administrator can set a label for the credential by setting a value in the `User Label` field and saving the data.
- Open the `Users -> (user) -> Credentials` tab.
- View the `Manage WebAuthn Authenticator` area.
A user carries out the following operations on the <<_account-service, `User Account Service`>> :
- View the `Account` page.
==== Edit Registered WebAuthn Authenticator
A user can edit the following information :
- Label (WebAuthn authenticator's label the user entered on registering it)
A user carries out the following operations on the <<_account-service, `User Account Service`>> :
- View the `Account` page.
- Edit the text in `Public Key Credential Label`.
- Click `Save`.
==== Delete Registered WebAuthn Authenticator
An administrator can delete the users' registered WebAuthn authenticators.
An Administrator carries out the following operations on the `Admin Console` :
- Open the `Users -> (user) -> Credentials` tab.
- On the `Disable Credentials` area, add `webauthn` to `Disable Types`.
- Click `Disable Credential Types`.
==== Re-Register WebAuthn Authenticator
A user can re-register their WebAuthn authenticator. Newly registered WebAuthn authenticator overrides the old registered one.
At first, an administrator carries out the following operations on the `Admin Console` to require the user to register their WebAuthn authenticator after their login :
- Open the `Users -> (user) -> Details` tab.
- Set `WebAuthn Register` on `Required User Actions`.
- Click `Save`.
After that, the user carries out the following operations :
- Open the login form.
- Fill in items, click `Save` and click `Login`.
- When the users log in, they are required to register their WebAuthn authenticator.
- After successful registration, the user's browser asks the user to enter the text as their just registered WebAuthn authenticator's label.
==== Configuration
===== Managing Policy
An administrator can configure WebAuthn related operations as `WebAuthn Policy` per realm.
@ -187,3 +143,46 @@ The configurable items and their description follow.
|The white list of AAGUID of which a WebAuthn authenticator can be registered. This is applied to the operation of registering WebAuthn authenticator. If no entry is set on this list, any WebAuthn authenticator can be registered.
|===
==== Managing WebAuthn credentials as a user
===== Register WebAuthn Authenticator
The appropriate method to register a WebAuthn authenticator depends on if the user has or has not already registered an account on {project_name}.
New user::
A new user carries out the following operations :
- Open the login form.
- Click the `Register` link.
- Fill in items on the register form and click `Register`.
- The user's browser asks the user to register their WebAuthn authenticator.
- After successful registration, the user's browser asks the user to enter the text as their just registered WebAuthn authenticator's label.
Existing user::
When existing users try to log in, they are required to register their WebAuthn authenticator automatically :
- Open the login form.
- Fill in items, click `Save` and click `Login`.
- When the users log in, they are required to register their WebAuthn authenticator.
- After successful registration, the user's browser asks the user to enter the text as their just registered WebAuthn authenticator's label.
===== View Registered WebAuthn Authenticator
A user carries out the following operations on the <<_account-service, `User Account Service`>> :
- View the `Account` page.
===== Edit Registered WebAuthn Authenticator
A user can edit the following information :
- Label (WebAuthn authenticator's label the user entered on registering it)
A user carries out the following operations on the <<_account-service, `User Account Service`>> :
- View the `Account` page.
- Edit the text in `Public Key Credential Label`.
- Click `Save`.

View file

@ -72,8 +72,10 @@ In order to configure a first login flow in which users are automatically linked
Create User If Unique::
This authenticator ensures that unique users are handled. Set the authenticator requirement to "Alternative".
Automatically Link Brokered Account::
Automatically link brokered identities without any validation with this authenticator. This is useful in an intranet environment of multiple user databases each with overlapping usernames/email addresses, but different passwords, and you want to allow users to use any password without having to validate. This is only reasonable if you manage all internal databases, and usernames/email addresses from one database matching those in another database belong to the same person. Set the authenticator requirement to "Alternative".
Automatically Set Existing User::
Automatically sets an existing user to the authentication context without any verification. Set the authenticator requirement to "Alternative".
NOTE: The described setup uses two authenticators, and is the simplest one, but it is possible to use other
authenticators according to your needs. For example, you can add the Review Profile authenticator to the beginning of the flow if you still want end users to confirm their profile information.
authenticators according to your needs. For example, you can add the Review Profile authenticator to the beginning of the flow if you still want
end users to confirm their profile information. You can also add authentication mechanisms to this flow, forcing a user to verify his credentials. This
would require a more complex flow, for example setting the "Automatically Set Existing User" and "Password Form" as "Required" in an "Alternative" sub-flow.

View file

@ -7,9 +7,31 @@ When viewing a user if you go to the `Credentials` tab you can manage a user's c
.Credential Management
image:{project_images}/user-credentials.png[]
==== Changing Passwords
The credentials are listed in a table, which has the following fields:
To change a user's password, type in a new one. A `Reset Password` button will show up that you click after you've typed everything in.
Position::
The arrow buttons in this column allows you to shift the priority of the credential for the user, with the topmost credential having the highest priority.
This priority determines which credential will be shown first to a user in case of a choice during login. The highest priority of those available to the
user will be the one selected.
Type::
This shows the type of the credential, for example `password` or `otp`.
User Label::
This is an assignable label to recognise the credential when presented as a selection option during login. It can be set to any value to describe the
credential.
Data::
This shows the non-confidential technical information about the credential. It is originally hidden, but you can press `Show data...` to reveal it for a
credential.
Actions::
This column has two buttons. `Save` records the value of the User Label, while `Delete` will remove the credential.
==== Creating a Password for the User
If a user doesn't have a password, or if his password has been deleted, the `Set Password` section will be shown on the page.
.Credential Management - Set Password
image:{project_images}/user-credentials-set-password.png[]
To create a password for a user, type in a new one. Click on the `Set Password` button after you've typed everything in.
If the `Temporary` switch is on, this new password can only be used once and the user will be asked to change their password after they have
logged in.
@ -18,13 +40,21 @@ them to reset their password. Choose `Update Password` from the `Reset Actions`
set the validity of the e-mail link which defaults to the one preset in `Tokens` tab in the realm settings.
The sent email contains a link that will bring the user to the update password screen.
==== Changing OTPs
Note that a user can only have a single credential of type password.
You cannot configure One-Time Passwords for a specific user within the Admin Console. This is the responsibility of the user.
If the user has lost their OTP generator all you can do is disable OTP for them on the `Credentials` tab.
If OTP is optional in your realm, the user will have to go to the User Account Management service to re-configure a new
OTP generator. If OTP is required, then the user will be asked to re-configure a new OTP generator when they log in.
==== Creating other credentials
You cannot configure other types of credentials for a specific user within the Admin Console. This is the responsibility of the user.
You can only delete credentials for a user on the `Credentials` tab, for example if the user has lost his OTP device, or if a credential
has been compromised.
===== Creating an OTP
If OTP is conditional in your realm, the user will have to go to the User Account Management service to re-configure a new
OTP generator. A user can set up any number of OTP credentials in this manner. If OTP is required, then the user will be asked
to re-configure a new OTP generator when they log in.
Like passwords, you can alternatively send an email to the user that will ask them to reset their OTP generator. Choose
`Configure OTP` in the `Reset Actions` list box and click the `Send Email` button. The sent email
contains a link that will bring the user to the OTP setup screen.
contains a link that will bring the user to the OTP setup screen. You can use this method even if the user already has an OTP credential,
and would like to set up some more.

View file

@ -37,10 +37,11 @@ Execution::
Execution Requirement::
Each execution defines how an authenticator behaves in a flow.
The requirement defines whether the authenticator is enabled, disabled, optional, required, or an alternative.
An alternative requirement means that the authentiactor is optional unless no other alternative authenticator is successful in the flow.
For example, cookie authentication, kerberos, and the set of all login forms are all alternative.
If one of those is successful, none of the others are executed.
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.
@ -59,9 +60,11 @@ Let's assume the following flows, executions and sub flows.
Cookie - ALTERNATIVE
Kerberos - ALTERNATIVE
Forms Subflow - ALTERNATIVE
Forms subflow - ALTERNATIVE
Username/Password Form - REQUIRED
OTP Password Form - OPTIONAL
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.
@ -108,13 +111,26 @@ Let's walk through the steps from when a client first redirects to keycloak to a
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 the OTP Form.
. The next execution is a subflow called Conditional OTP. The executions for this subflow are loaded and the same processing logic occurs. It's 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 evaluates 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 login.
. 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 also 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.
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.
@ -130,13 +146,25 @@ Let's walk through the steps from when a client first redirects to keycloak to a
=== Authenticator SPI Walk Through
In this section, we'll take a look at the Authenticator interface.
For this, we are going to implement an authenticator that requires that a user enter in the answer to a secret question like "What is your mother's maiden name?". This example is fully implemented and contained in the examples/providers/authenticator directory of the demo distribution of {project_name}.
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 {project_name}.
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.
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. But some, like 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.
@ -144,7 +172,7 @@ This jar must contain a file named `org.keycloak.authentication.AuthenticatorFa
This file must list the fully qualified class name of each AuthenticatorFactory implementation you have in the jar.
For example:
[source]
[source,java]
----
org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
org.keycloak.examples.authenticator.AnotherProviderFactory
@ -154,30 +182,333 @@ This services/ file is used by {project_name} to scan the providers it has to lo
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 Keycloak
* `credential_data` contains a json with the static information of the credential that can be shared in the admin console or via the REST API.
* `priority` defines how "preferred" a credential is for a user, to determine which credential to present when a user has multiple choices.
As the secret_data and credential_data fields are designed to contain json, it is up to you to determine how to structure, read and write into
these fields, allowing you a lot of flexibility.
For this example, we are going to use a very simple credential data, containing only the question asked to the user:
[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 Keycloak. In this case, the secret data would also have to contain a field for the salt, and the credential data information
about the algorithm such as the type of algorithm used and the number of iterations used. For more details you can consult the implementation of the
`org.keycloak.models.credential.PasswordCredentialModel` class.
In our case we create the class `SecretQuestionCredentialModel`:
[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 it's parent class,
and the unmarshalled objets in it's own attributes. This leads us to create method a 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 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;
}
private UserCredentialStore getCredentialStore() {
return session.userCredentialManager();
}
----
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 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 KeycloakSession's `userCredentialManager`, 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 getCredentialStore().createCredential(realm, user, credentialModel);
}
@Override
public void deleteCredential(RealmModel realm, UserModel user, String credentialId) {
getCredentialStore().removeStoredCredential(realm, user, 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.
For this example, we need to check if the answer to the secret question has been set up by the user or not.
In our case we are storing this information, hashed, within a UserCredentialValueModel within the UserModel (just like passwords are stored). Here's how we do this very simple check:
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 session.userCredentialManager().isConfiguredFor(realm, user, "SECRET_QUESTION");
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return getCredentialProvider(session).isConfiguredFor(realm, user, getType(session));
}
----
The configuredForCredentialType() call queries the user to see if it supports that credential type.
The next method to implement on the Authenticator is setRequiredActions(). If configuredFor() returns false and our example authenticator is required within the flow, this method will be called.
It is responsible for registering any required actions that must be performed by the user.
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.
@ -192,30 +523,31 @@ Here is the implementation of the setRequiredActions() method.
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.
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);
@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;
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.
@ -223,8 +555,7 @@ If that returns true, we just mark this execution's status as SUCCESS using the
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.
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.
@ -238,29 +569,19 @@ The flow will end up invoking the action() method of our Authenticator implement
[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();
}
protected boolean validateAnswer(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String secret = formData.getFirst("secret_answer");
UserCredentialModel input = new UserCredentialModel();
input.setType("SECRET_QUESTION");
input.setValue(secret);
return context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), input);
@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.
@ -269,22 +590,52 @@ failureChallenge() works the same as challenge(), but it also records the failur
If validation is successful, then we set a cookie to remember that the secret question has been answered and we call AuthenticationFlowContext.success().
The last thing I want to go over is the setCookie() method.
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 user to log the user.
In case there is more than one, {project_name} allows the user to choose during the login which credential he is using, 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 = context.getSelectedCredentialId();
if (credentialId == null || credentialId.isEmpty()) {
credentialId = getCredentialProvider(context.getSession())
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
context.setSelectedCredentialId(credentialId);
}
UserCredentialModel input = new UserCredentialModel(credentialId, getType(context.getSession()), secret);
return getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input);
}
----
The last thing to go over is the setCookie() method.
This is an example of providing configuration for the Authenticator.
In this case we want the max age of the cookie to be configurable.
[source,java]
----
protected void setCookie(AuthenticationFlowContext context) {
AuthenticatorConfigModel config = context.getAuthenticatorConfig();
int maxCookieAge = 60 * 60 * 24 * 30; // 30 days
if (config != null) {
maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
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 ...
}
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.
@ -321,14 +672,16 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
----
The next thing the factory is responsible for is to 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.
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 a 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
@ -428,6 +781,16 @@ When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvid
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,java]
----
<#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 Authenticator to a Flow
@ -452,7 +815,7 @@ This jar does not have to be separate from other provider classes but it must co
This file must list the fully qualified classname of each RequiredActionFactory implementation you have in the jar.
For example:
[source]
[source,java]
----
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory
----

View file

@ -36,6 +36,35 @@ please take a look at link:{developerguide_jsproviders_link}[{developerguide_jsp
In the previous releases, developers were allowed to provide client credentials to the JavaScript adapter. For now on, this capability was *removed*, because client-side apps are not safe to keep secrets.
==== Authentication flows changes
We did some refactoring and improvements related to the authentication flows, which requires some attention during migration.
OPTIONAL execution requirement removed::
Regarding migration, the most important change is removing the support for OPTIONAL requirement from authentication executions and
replacing it with the CONDITIONAL requirement, which allows more flexibility. The existing OPTIONAL authenticators configured in previous version will be replaced with the CONDITIONAL subflows, where particular subflow will have
the `Condition - User Configured` condition configured as first execution, and the previously OPTIONAL authenticator (for example `OTP Form`) as second execution.
From the user's point of view, the behaviour during authentication should be same as in previous version.
Changes in the Java SPI::
There are some changes in the Java Authentication SPI and Credential Provider SPI. The interface `Authenticator` is not changed,
but you may be affected if you're developing more advanced authenticators, which introduce some new credential types (subclasses of `CredentialModel`).
There are changes on the `CredentialProvider` interface and introduction of some new interfaces like `CredentialValidator`. Also you
may be affected if your authenticator supported the OPTIONAL execution requirement. It is recommended to double check latest authentication
examples in the server development guide for more details.
Freemarker template changes::
There are also some changes in the freemarker templates. You may be affected if you have your own theme with custom freemarker
templates for login forms or some account forms, especially for the forms related to OTP. It is recommended to double check changes in
the Freemarker templates in latest {project_name} and align your templates according to it.
==== User credentials changes
We added more flexibility around storing of user credentials. Among other things, every user can have multiple credentials of same
type, for example multiple OTP credentials. There are some changes in the database schema in relation to that, however the credentials from
previous version should be automatically updated to the new format and users should be still able to login with their passwords or OTP
credentials set in previous version.
=== Migrating to 7.0.0
==== Upgrade to Wildfly 17