January 25, 2025

Using Action Sets in Keycloak Authentication: Managing Multi-Step Flows

 

Keycloak provides powerful mechanisms for implementing custom authentication flows through action sets and authentication sessions. This flexibility allows you to handle simple two-step authentication processes as well as complex multi-step flows, such as Passwordless, Multi-Factor Authentication (MFA), and OTP-based authentication. Let’s dive into how you can achieve these flows using action sets, discuss best practices, and explore some advanced strategies.


What are Action Sets in Keycloak?

Action sets are used to define steps in an authentication flow. Each step in the flow can be assigned specific actions, such as verifying user credentials, sending an OTP, or checking device trust. Keycloak chains these actions together to build custom flows.

Key Concepts:

  1. Action: A single step in the authentication process (e.g., password validation, OTP validation).
  2. Action Set: A collection of actions grouped as part of an authentication step.
  3. Execution Flow: Defines how steps and actions are executed in sequence.

Setting Up Multi-Step Authentication Flows

Example: 2-Step Flow (Password + OTP)

Step 1: Validate Password

  • Action: PasswordForm
    • This form accepts the user’s username and password for validation.
    • Execution Requirement: Required

Step 2: Validate OTP

  • Action: OTPForm
    • This step generates and validates an OTP sent to the user’s registered email or phone.
    • Execution Requirement: Required
{
  "alias": "Password + OTP",
  "authenticationExecutions": [
    {
      "authenticator": "password-form",
      "requirement": "REQUIRED",
      "priority": 10
    },
    {
      "authenticator": "otp-form",
      "requirement": "REQUIRED",
      "priority": 20
    }
  ]
}

Example: 3-Step Flow (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
    • Generates and validates an OTP for passwordless login.
    • Execution Requirement: Required

Step 3: Device Trust Validation

  • Action: DeviceTrustCheck
    • Checks whether the login is from a trusted device or browser.
    • Execution Requirement: Conditional (skipped for trusted devices)
{
  "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
    }
  ]
}

Best Practices for Multi-Step Flows

  1. Keep it User-Friendly

    • Minimize the number of steps unless necessary.
    • Use conditional actions to skip unnecessary steps (e.g., skipping OTP for trusted devices).
  2. Ensure Security

    • Validate inputs at each step.
    • Use short-lived tokens for OTPs to prevent replay attacks.
    • Implement rate limiting to avoid brute-force attacks.
  3. Leverage Authentication Sessions

    • Use the AuthenticationSessionModel to persist state across steps.
    • Store temporary data like OTP codes or device trust flags in the session to avoid re-fetching from the database.
  4. Error Handling

    • Return descriptive error messages without revealing sensitive information.
    • Log errors using Keycloak’s event system.

Code Samples

Persisting State Across Steps

You can use AuthenticationSessionModel to store and retrieve state:

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);
        }
    }
}

Conditional Flows

Implement conditions to skip steps dynamically:

pup)l != null;
    }
}

When to Use Authentication Sessions

Scenarios:

  1. Multi-Step Authentication

    • Store intermediate data like OTPs, temporary user information, or device trust flags.
  2. Custom State Management

    • Use AuthNotes for short-lived state (e.g., per-login session data).
    • Use user attributes for longer-lived data (e.g., device trust information).

Best Practices:

  • Avoid Sensitive Data in Sessions: Do not store passwords or sensitive tokens in session attributes.
  • Clean Up: Remove session attributes once they are no longer needed to prevent state leakage.
  • Log Events: Track state changes and authentication events using Keycloak’s event logging.

Keycloak’s flexibility with action sets and authentication sessions enables you to design secure, user-friendly, and highly customizable authentication flows. By leveraging these tools effectively, you can handle diverse scenarios, from simple password authentication to sophisticated passwordless and multi-factor workflows.