Wocha Docs

Step-up authentication

Request higher authentication assurance (AAL2/AAL3) via acr_values in OAuth flows

Step-up authentication asks a user who is already signed in to complete an additional verification before accessing a sensitive resource. Wocha implements step-up through standard OAuth acr_values parameters and NIST-aligned ACR/AMR token claims.


When to use step-up

Use step-up when an operation requires stronger assurance than the user's current session provides:

ScenarioRecommended ACR
View account settingsurn:wocha:aal1 (default)
Change password or add MFAurn:wocha:aal2
Transfer funds or delete dataurn:wocha:aal2
Admin key management, privilege escalationurn:wocha:aal3
Adaptive risk triggerDetermined by risk engine

Step-up is distinct from a full re-login. Wocha preserves the existing session where possible and elevates the authenticator assurance level (AAL) to meet the requested ACR.


Requesting ACR levels in OAuth

Pass acr_values in the authorise request. Wocha checks the current session ACR at consent time and redirects to MFA step-up when the session is insufficient.

GET /oauth2/auth?
  client_id=YOUR_CLIENT_ID&
  response_type=code&
  scope=openid+profile&
  redirect_uri=https://app.example.com/callback&
  acr_values=urn:wocha:aal2&
  state=RANDOM_STATE

Multiple values express preference order (first value has highest priority):

acr_values=urn:wocha:aal3 urn:wocha:aal2

Wocha also accepts legacy shorthand values (aal1, aal2, aal3) and maps them to Wocha URNs internally.

  1. Client sends acr_values=urn:wocha:aal2 in the authorise request
  2. User authenticates (if not already signed in)
  3. At consent, Wocha compares currentAcr against the requested value
  4. If insufficient, the user is redirected to AAL2 step-up with the strongest enrolled factor
  5. After MFA completion, consent proceeds and tokens include the elevated acr and amr claims

Next.js

Protect a server action or route handler by checking the session ACR, then redirecting to step-up when needed:

import { getSession } from "@wocha/nextjs";
import { requireAal2, ACR_AAL2 } from "@wocha/nextjs";
import { redirect } from "next/navigation";
 
export async function deleteAccount() {
  const session = await getSession();
  if (!session) redirect("/login");
 
  if (!requireAal2(session)) {
    const stepUpUrl = new URL(`${process.env.WOCHA_ISSUER}/oauth2/auth`);
    stepUpUrl.searchParams.set("client_id", process.env.WOCHA_CLIENT_ID!);
    stepUpUrl.searchParams.set("response_type", "code");
    stepUpUrl.searchParams.set("scope", "openid");
    stepUpUrl.searchParams.set("acr_values", ACR_AAL2);
    stepUpUrl.searchParams.set(
      "redirect_uri",
      `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback`,
    );
    redirect(stepUpUrl.toString());
  }
 
  // Proceed with account deletion...
}

For middleware-level protection:

import { withWochaAuth } from "@wocha/nextjs/middleware";
import { requireAal3 } from "@wocha/nextjs";
 
export default withWochaAuth(async (req, session) => {
  if (req.nextUrl.pathname.startsWith("/admin/keys")) {
    if (!requireAal3(session)) {
      const url = req.nextUrl.clone();
      url.pathname = "/step-up";
      url.searchParams.set("acr", "urn:wocha:aal3");
      return Response.redirect(url);
    }
  }
});

Express

import express from "express";
import { wochaSession, requireAal2, ACR_AAL2 } from "@wocha/express";
 
const app = express();
 
app.delete("/api/billing/payment-method", wochaSession(), (req, res) => {
  if (!requireAal2(req.wocha.session)) {
    const stepUpUrl = new URL(`${process.env.WOCHA_ISSUER}/oauth2/auth`);
    stepUpUrl.searchParams.set("client_id", process.env.WOCHA_CLIENT_ID!);
    stepUpUrl.searchParams.set("response_type", "code");
    stepUpUrl.searchParams.set("scope", "openid");
    stepUpUrl.searchParams.set("acr_values", ACR_AAL2);
    stepUpUrl.searchParams.set("redirect_uri", process.env.WOCHA_CALLBACK_URL!);
    return res.redirect(302, stepUpUrl.toString());
  }
 
  // Remove payment method...
  res.status(204).end();
});

React

Gate sensitive UI and trigger step-up from the client:

"use client";
 
import { useSession } from "@wocha/react";
 
const ACR_AAL2 = "urn:wocha:aal2";
 
function BillingPanel() {
  const { data: session, status } = useSession();
 
  if (status !== "authenticated") return null;
 
  const acr = session?.acr ?? "urn:wocha:aal1";
  const needsStepUp = !acr.includes("aal2") && !acr.includes("aal3");
 
  if (needsStepUp) {
    const stepUpUrl =
      `${process.env.NEXT_PUBLIC_WOCHA_ISSUER}/oauth2/auth?` +
      `client_id=${process.env.NEXT_PUBLIC_WOCHA_CLIENT_ID}&` +
      `response_type=code&scope=openid&` +
      `acr_values=${encodeURIComponent(ACR_AAL2)}&` +
      `redirect_uri=${encodeURIComponent(window.location.origin + "/callback")}`;
 
    return (
      <div>
        <p>Verify your identity to manage billing.</p>
        <a href={stepUpUrl}>Verify with MFA</a>
      </div>
    );
  }
 
  return <PaymentMethodList />;
}

For adaptive MFA, Wocha handles step-up automatically at consent — your app only needs explicit acr_values for operations that always require a specific assurance level.


ACR and AMR reference

ACR values

ValueLevelDescription
urn:wocha:aal11Single-factor authentication (password, passkey, or social)
urn:wocha:aal22Two-factor authentication (password + TOTP, SMS, or email OTP)
urn:wocha:aal33Hardware-bound authentication (password + hardware WebAuthn key)

Legacy values accepted for compatibility:

LegacyMaps to
aal1urn:wocha:aal1
aal2, urn:mace:incommon:iap:silverurn:wocha:aal2
aal3, urn:mace:incommon:iap:goldurn:wocha:aal3

AMR values (RFC 8176)

AMRAuthentication method
pwdPassword
otpTime-based or email one-time password
smsSMS one-time password
hwkHardware WebAuthn/FIDO2 key
swkSoftware passkey (platform authenticator)
fedFederated identity (OIDC/social)
mfaMultiple distinct authentication factors used

Token claims

After successful step-up, ID and access tokens include:

{
  "acr": "urn:wocha:aal2",
  "amr": ["pwd", "otp", "mfa"],
  "auth_time": 1705320000
}

On this page