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.
AuthenticationProcessor.authenticate() drives one flow pass
services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java · lines 958–963authenticateOnly() 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.
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.
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();}
processResult() maps an Authenticator's verdict to what happens next
services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java · lines 527–556Every 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.
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.
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);
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 36–88LoginActionsService wires an HTTP request into a fresh AuthenticationProcessor
services/src/main/java/org/keycloak/services/resources/LoginActionsService.java · lines 356–395A 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.
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'.
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);}
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?