Using Action Sets in Keycloak Authentication: Configuring Multi-Step Flows
Keycloak offers powerful mechanisms to implement custom authentication flows using action sets and authentication sessions. With this flexibility, you can create both simple and advanced authentication processes, such as passwordless login, multi-factor authentication (MFA), and OTP-based verification. This guide provides a detailed walkthrough on configuring these action sets, alongside best practices and advanced strategies to maximize security and usability.
What Are Action Sets in Keycloak?
Action sets in Keycloak define the individual steps that comprise an authentication flow. By combining and sequencing these steps, you can create tailored workflows that meet specific requirements.
Key Concepts
- Action: A single step in an authentication process, such as verifying credentials or sending an OTP.
- Action Set: A group of actions that belong to an authentication step.
- Execution Flow: Defines the sequence in which actions are executed.
Configuring Multi-Step Authentication Flows
Example 1: Two-Step Authentication Flow (Password + OTP)
Step 1: Validate Password
- Action:
PasswordForm
- Accepts the username and password from the user for validation.
- Execution Requirement: Required
Step 2: Validate OTP
- Action:
OTPForm
- Sends an OTP to the user’s registered email or phone and validates it.
- Execution Requirement: Required
Configuration Example
{
"alias": "Password + OTP",
"authenticationExecutions": [
{
"authenticator": "password-form",
"requirement": "REQUIRED",
"priority": 10
},
{
"authenticator": "otp-form",
"requirement": "REQUIRED",
"priority": 20
}
]
}
Example 2: Passwordless Login with Device Trust
Step 1: Username/Email Input
- Action:
UserNameForm
- Allows the user to input their username or email.
- Execution Requirement: Required
Step 2: OTP Validation
- Action:
OTPForm
- Sends and validates an OTP for passwordless login.
- Execution Requirement: Required
Step 3: Device Trust Validation
- Action:
DeviceTrustCheck
- Verifies if the login is from a trusted device or browser.
- Execution Requirement: Conditional (skipped for trusted devices)
Configuration Example
{
"alias": "Passwordless with Device Trust",
"authenticationExecutions": [
{
"authenticator": "username-form",
"requirement": "REQUIRED",
"priority": 10
},
{
"authenticator": "otp-form",
"requirement": "REQUIRED",
"priority": 20
},
{
"authenticator": "device-trust-check",
"requirement": "CONDITIONAL",
"priority": 30
}
]
}
Configuring Action Sets in the Keycloak Admin Console
Step 1: Create a New Authentication Flow
- Navigate to Authentication in the Keycloak Admin Console.
- Click on the Flows tab.
- Click New to create a custom authentication flow.
- Alias: Enter a name for your flow (e.g.,
Password + OTP
). - Built-In: Choose whether this flow is built-in or not (leave unchecked for custom flows).
- Alias: Enter a name for your flow (e.g.,
Step 2: Add Execution Steps
- Select your newly created flow and click Add Execution.
- In the Add Execution dialog, choose an authenticator (e.g.,
password-form
,otp-form
). - Set the Requirement:
- Required: Mandatory step.
- Conditional: Executed only if conditions are met.
- Alternative: One of several possible steps.
- Save your changes.
Step 3: Configure Execution Order
- Use the Priority field to define the order of execution.
- Lower numbers are executed first.
Step 4: Save and Test
- Assign the custom flow to the desired client or realm under Bindings.
- Test the flow by logging in with a user account.
Dynamically Updating Action Sets in Custom Sign-In Flows
Keycloak allows for dynamic updates to action sets, enabling custom sign-in flows that adapt based on real-time conditions. For instance, you can programmatically determine which actions or steps to include in a flow based on user roles, device type, or risk assessment.
Example: Dynamic Action Injection
Use Case: Adding an OTP Step for High-Risk Logins
Suppose you want to dynamically add an OTP validation step for users logging in from unknown devices or high-risk locations.
Implementation Steps:
- Create a Custom Authenticator: Implement a custom authenticator to evaluate conditions and modify the flow dynamically.
public class DynamicActionAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
boolean isHighRisk = evaluateRisk(context);
if (isHighRisk) {
context.getAuthenticationSession().setAuthNote("add-otp-step", "true");
}
context.success();
}
private boolean evaluateRisk(AuthenticationFlowContext context) {
// Implement logic to evaluate risk (e.g., check IP, device trust, or user behavior).
return true; // Example: Treat all logins as high-risk for demonstration.
}
}
- Modify the Flow Based on Conditions:
In subsequent steps, check the
AuthNote
to determine whether to include additional actions.
public class ConditionalOTPAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
String addOtpStep = context.getAuthenticationSession().getAuthNote("add-otp-step");
if ("true".equals(addOtpStep)) {
String otp = generateOtp();
context.getAuthenticationSession().setAuthNote("otp", otp);
sendOtpToUser(context.getUser(), otp);
context.challenge(context.form().createForm("otp-form.ftl"));
} else {
context.success();
}
}
@Override
public void action(AuthenticationFlowContext context) {
String inputOtp = context.getHttpRequest().getDecodedFormParameters().getFirst("otp");
String sessionOtp = context.getAuthenticationSession().getAuthNote("otp");
if (inputOtp != null && inputOtp.equals(sessionOtp)) {
context.success();
} else {
context.failure(AuthenticationFlowError.INVALID_CREDENTIALS);
}
}
}
- Configure the Flow:
Add the
DynamicActionAuthenticator
as the first step in your custom flow andConditionalOTPAuthenticator
as a later step. Ensure the OTP step is configured as Conditional.
Configuring Action Sets in a Custom Provider JAR
To extend Keycloak with custom authentication logic, you can package your custom authenticators and action sets in a provider JAR.
Steps to Create a Provider JAR
1. Implement Custom Authenticators
Create Java classes that implement the Authenticator
or AuthenticatorFactory
interfaces.
public class CustomAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
// Custom authentication logic
context.success();
}
@Override
public void action(AuthenticationFlowContext context) {
// Action to perform during this step
}
@Override
public void close() {
// Clean-up resources if necessary
}
}
2. Register Authenticators in a Factory
Provide metadata and configuration for your authenticators.
public class CustomAuthenticatorFactory implements AuthenticatorFactory {
@Override
public Authenticator create(KeycloakSession session) {
return new CustomAuthenticator();
}
@Override
public String getId() {
return "custom-authenticator";
}
@Override
public void init(Config.Scope config) {}
@Override
public void postInit(KeycloakSessionFactory factory) {}
@Override
public void close() {}
}
3. Package the Provider as a JAR
- Compile your classes and package them into a JAR file.
- Include a
META-INF/services/org.keycloak.authentication.AuthenticatorFactory
file listing your factory class.
4. Deploy the JAR
- Copy the JAR file to the
providers
directory of your Keycloak server. - Restart Keycloak to load the custom provider.
5. Configure in Admin Console
- Your custom authenticators will now appear as options in the Add Execution dialog when configuring authentication flows.
Best Practices for Multi-Step Authentication
-
Simplify User Experience
- Reduce the number of steps to the minimum required for security.
- Use conditional actions to skip unnecessary steps (e.g., skipping OTP for trusted devices).
-
Enhance Security
- Validate inputs at every step.
- Use short-lived tokens for OTPs to mitigate replay attacks.
- Implement rate limiting to prevent brute-force attacks.
-
Leverage Authentication Sessions
- Use
AuthenticationSessionModel
to persist data across steps (e.g., OTP codes, device trust flags). - Store temporary data in session attributes rather than fetching it repeatedly from the database.
- Use
-
Handle Errors Gracefully
- Display user-friendly error messages.
- Log errors using Keycloak’s event logging system to aid troubleshooting.
Advanced Strategies for Custom Flows
Persisting State Across Steps
Use AuthenticationSessionModel
to manage temporary state during multi-step authentication.
Example: OTP Storage and Validation
public class OTPAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
String otp = generateOtp();
AuthenticationSessionModel session = context.getAuthenticationSession();
session.setAuthNote("otp", otp);
sendOtpToUser(context.getUser(), otp);
context.challenge(context.form().createForm("otp-form.ftl"));
}
@Override
public void action(AuthenticationFlowContext context) {
String inputOtp = context.getHttpRequest().getDecodedFormParameters().getFirst("otp");
String sessionOtp = context.getAuthenticationSession().getAuthNote("otp");
if (inputOtp != null && inputOtp.equals(sessionOtp)) {
context.success();
} else {
context.failure(AuthenticationFlowError.INVALID_CREDENTIALS);
}
}
}
Conclusion
By leveraging action sets and authentication sessions, Keycloak enables the creation of secure, user-friendly, and flexible authentication flows. Whether implementing simple two-step verification or advanced passwordless login with device trust, following the best practices outlined in this guide will help ensure robust and reliable authentication processes. Start experimenting with custom flows to unlock the full potential of Keycloak!