Keycloak Onboarding LabChapter III — Authentication Flow Engine0/5Contents
Chapter III

Authentication Flow Engine

AuthenticationProcessor is the runtime engine that drives every browser login in Keycloak: it walks a configurable AuthenticationFlowModel tree, creates pluggable Authenticator SPI instances for each execution step, and interprets the FlowStatus each one returns (success, challenge, failure, fork, reset) to decide what happens next. LoginActionsService is the JAX-RS front door that wires incoming HTTP requests (GET for a fresh challenge, POST for a form action) into a freshly configured AuthenticationProcessor and turns its result into an HTTP response, handing rendering off to the LoginFormsProvider theme layer. Understanding this pair is the key to understanding how password, OTP, WebAuthn, and identity-broker logins are all just different Authenticator plugins running through the same orchestration machinery — the clearest live example of Keycloak's Provider/SPI pattern in the busiest code path in the server.


walkthrough

AuthenticationProcessor.authenticate() drives one flow pass

services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java · lines 958963
line 960

authenticateOnly() resolves the configured flow (via createFlowExecution) and calls processFlow() on it. If any Authenticator in the flow needs to show a form, that Response challenge bubbles all the way back up here.

line 961

A non-null challenge means the flow paused to render a page (e.g. the login form or an OTP prompt) — the processor returns immediately and waits for the next HTTP request to resume where it left off.

line 962

If every required Authenticator in the flow already succeeded, authenticationComplete() runs required actions / consent and finally calls the LoginProtocol to finish the login.

public Response authenticate() throws AuthenticationFlowException {    logger.debug("AUTHENTICATE");    Response challenge = authenticateOnly();    if (challenge != null) return challenge;    return authenticationComplete();}
step 1 of 3
walkthrough

processResult() maps an Authenticator's verdict to what happens next

services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java · lines 527556
lines 527–530

Every Authenticator calls context.success()/failure()/challenge() on the AuthenticationProcessor.Result it's handed; DefaultAuthenticationFlow reads that FlowStatus back and records it against the execution so the flow tree knows what's been satisfied.

lines 531–536

A FAILED verdict either re-renders the current form with an error (if the authenticator supplied a challenge Response) or throws, which unwinds up to LoginActionsService's exception handler.

lines 537–539

CHALLENGE is the normal 'show a form' case — e.g. the password form asking for credentials, or the OTP form asking for a code. sendChallenge() records which execution is now pending and returns its Response.

switch (status) {    case SUCCESS:        setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.SUCCESS);        return null;    case FAILED:        processor.logFailure(execution.getAuthenticator());        setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.FAILED);        if (result.getChallenge() != null) {            return sendChallenge(result, execution);        }        throw new AuthenticationFlowException(result.getError(), result.getEventDetails(), result.getUserErrorMessage());    case FORCE_CHALLENGE:    case CHALLENGE:        setExecutionStatus(execution, AuthenticationSessionModel.ExecutionStatus.CHALLENGED);        return sendChallenge(result, execution);
step 1 of 3
public interface Authenticator extends Provider {    void authenticate(AuthenticationFlowContext context);    void action(AuthenticationFlowContext context);    boolean requiresUser();    boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user);    void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user);}
server-spi-private/src/main/java/org/keycloak/authentication/Authenticator.java · lines 3688
Figure I · Every step in a flow — password entry, OTP verification, WebAuthn assertion, IdP redirect — is an implementation of this one SPI. authenticate() is the first call for a step; action() handles the form post-back. AuthenticationProcessor never has type-specific knowledge of any of them; it only knows this interface, which is why new login methods can be added as plugins without touching the engine.
walkthrough

LoginActionsService wires an HTTP request into a fresh AuthenticationProcessor

services/src/main/java/org/keycloak/services/resources/LoginActionsService.java · lines 356395
lines 357–366

A brand-new AuthenticationProcessor is created and configured per HTTP request, hydrated from the persisted AuthenticationSessionModel — that's how state survives across the multiple round-trips a login takes.

lines 368–372

GET /login-actions/authenticate (a fresh challenge) calls authenticate(); POST back from a submitted form (isActionRequest) calls authenticationAction(execution) instead — same processor, two distinct entry points for 'show me the next step' vs 'validate what the user submitted'.

lines 373–377

AuthenticationFlowException carries a typed AuthenticationFlowError; handleBrowserException translates it into a themed error page or, for FORK_FLOW, spins up a whole new AuthenticationProcessor to restart the browser flow (used by 'forgot password' style redirects).

protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {    processor.setAuthenticationSession(authSession)            .setFlowPath(flowPath)            .setBrowserFlow(true)            .setFlowId(flow.getId())            .setConnection(clientConnection)            .setEventBuilder(event)            .setRealm(realm)            .setSession(session)            .setUriInfo(session.getContext().getUri())            .setRequest(request);    Response response;    try {        if (action) {            response = processor.authenticationAction(execution);        } else {            response = processor.authenticate();        }    } catch (WebApplicationException e) {        response = e.getResponse();    } catch (Exception e) {        response = processor.handleBrowserException(e);    }    return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action, request);}
step 1 of 3
checkpoint

Check your understanding

Answered in place — nothing is graded, everything is explained. 0 / 2 passed

When an Authenticator like the OTP form needs to show a challenge page, which component actually renders the HTML?

Why can Keycloak add a brand-new credential type (e.g. a new WebAuthn variant) without modifying AuthenticationProcessor or DefaultAuthenticationFlow?