AuthGeek

Systems, Security, & Architecture

Beyond Passwords: Step-Up Auth with ACR Values

Stop building custom 2FA logic. Learn how to use OpenID Connect ACR values, Keycloak, and Open Policy Agent to implement true step-up authentication at the API Gateway.

For: Application security architects, identity engineers, and developers tired of rewriting MFA flows.

The 2FA Spaghetti Problem

Look at any legacy application’s codebase, and you’ll inevitably find the authentication spaghetti. The symptom is obvious: business logic intermingled with security checks.

// The Anti-Pattern
if (user.hasSession() && req.url === '/transfer-funds') {
    if (!user.hasRecentMFA() || user.mfaMethod !== 'TOTP') {
        return res.redirect('/prompt-mfa?returnTo=/transfer-funds');
    }
    // Proceed with transfer...
}

This pattern is a massive security liability. It requires every backend microservice to deeply understand the state of the user’s authentication session, track the recency of their multi-factor authentication, and handle the complex orchestration of redirecting them back and forth to an identity provider.

There has to be a better way. And there is: Authentication Context Class Reference (ACR) values.

Understanding ACR Values

ACR is a standard claim defined in the OpenID Connect (OIDC) specification. Rather than a boolean “is the user authenticated” flag, ACR provides a standardized vocabulary to describe the quality and strength of that authentication.

Think of it as Assurance Levels (LoA).

  • LoA1 (Low): User authenticated with an anonymous session or basic tracking cookie (our “Every User is a Machine” pattern).
  • LoA2 (Medium): User authenticated with a standard username and password.
  • LoA3 (High): User authenticated with password + Email OTP or SMS.
  • LoA4 (Very High): User authenticated with WebAuthn, Passkeys, or hardware tokens (FIDO2).

When Keycloak authenticates a user, it embeds this ACR claim directly into the JWT payload:

{
  "sub": "fa5b1695-1...9a",
  "preferred_username": "jane.doe",
  "acr": "loa3",
  "scope": "openid email profile"
}

The Zero-Trust Gateway Pattern

By combining APISIX (API Gateway), Keycloak (IdP), and Open Policy Agent (OPA), we can completely decouple the step-up authentication flow from the backend services.

1. The IdP (Keycloak)

Keycloak is configured with advanced Authentication Flows. If an application requests a specific ACR value (e.g., acr_values=loa3), Keycloak automatically prompts the user for the necessary step-up challenges—such as an Email OTP or a WebAuthn prompt—without the application needing to write a single line of MFA logic.

2. The Policy Engine (OPA)

OPA holds the business rules. It evaluates the incoming request against the required assurance level. For example, viewing an account summary might require loa2, but initiating a wire transfer strictly demands loa4.

# OPA Policy snippet
allow {
    input.request.method == "POST"
    input.request.path == "/api/wire-transfer"
    input.token.payload.acr == "loa4"
}

3. The Gateway (APISIX)

APISIX sits at the edge. It verifies the JWT signature, queries OPA for an authorization decision, and if the ACR level is insufficient, it catches the 403 Forbidden and intercepts the request, redirecting the user back to Keycloak with the required acr_values appended to the OIDC login URL.

The Result

The backend backend service (e.g., the wire transfer API) simply assumes that if the request reached it, the user has been strongly authenticated. It doesn’t know what TOTP is. It doesn’t know what WebAuthn is.

It just processes the transfer.

Next up, try it yourself in our Live Sandbox where we demonstrate these flows using APISIX and Keycloak.