Step-up authentication
Rakomi exposes step-up authentication — a short-lived re-auth token issued via one of four methods, used to gate sensitive actions (account-linking, passkey CRUD, future high-value mutations). The issued token carries an AAL claim (Authenticator Assurance Level, NIST SP 800-63-3) and an AMR claim (RFC 8176 Authentication Methods References) so future AAL-aware gates (including a risk engine) can enforce “this action requires AAL2”.
Methods
Section titled “Methods”| Method | Endpoint | AAL | AMR (RFC 8176) | Notes |
|---|---|---|---|---|
password | POST /v1/auth/step-up/password | 2 | pwd | Body: { password }. |
totp | (issued via login MFA verify) | 2 | otp | Time-based one-time password. Already in step-up token enum. |
magic_link | POST /v1/auth/step-up/magic-link/{initiate,verify} | 1 | mca | Single-use 10-min token. Distinct phishing-warning email template. |
email_otp | POST /v1/auth/step-up/email-otp/{initiate,verify} | 1 | otp | 6-digit code, 5-min TTL, hashed at rest. Distinct template. |
passkey | POST /v1/auth/step-up/passkey/{options,verify} | 2 | swk / hwk | WebAuthn assertion. UP+UV both required for AAL2 — UV=false is rejected, never AAL1. |
SMS step-up is intentionally NOT offered. SIM-swap and SS7 attacks reduce SMS to AAL1 with significant phishing risk. Use passkey or email-OTP instead. (FusionAuth supports SMS step-up; we deliberately diverge.)
Recommended order
Section titled “Recommended order”When the SDK gives you a list of available step-up methods on a 401 account_linking/mfa_required body (available_methods array), prefer
methods in this order — strongest first:
passkey— phishing-resistant, AAL2, single click on supported devices.email_otp— faster than magic-link (no inbox roundtrip), AAL1.magic_link— last resort if no passkey AND user prefers click-link UX, AAL1.password— present only when the user has a password set; AAL2.
Order in the available_methods response is a hint — the server may
personalize. The SDK exposes the list as
MfaStepUpRequiredError.availableMethods in @rakomi/node 0.12.0+.
AAL ↔ NIST / eIDAS / ACR mapping
Section titled “AAL ↔ NIST / eIDAS / ACR mapping”Two distinct
acr-related vocabularies exist, and they must not be confused. Theacrclaim actually stamped into an issued session/token (see OIDC provider guide —acr_values_supported) is a bare, unprefixed string —aal1/aal2(andeidas_low/eidas_substantial/eidas_highfor EUDI Wallet logins). Theurn:cre8eve:rakomi:*ACR URI column below is a different, request-side-only format used exclusively inside the RFC 9470WWW-Authenticate: acr_values="..."challenge header (see below) — a space-separated string, not a JSON array, and not the value your code will ever read back from anacrclaim on a token. If you’re integrating against theacrclaim on an issued token, use the “acr claim value” column, not “ACR URI”.
Rakomi aal claim | NIST 800-63-3 | eIDAS LoA | acr claim value (on the issued token) | ACR URI (WWW-Authenticate challenge only) | Methods |
|---|---|---|---|---|---|
1 | AAL1 | Substantial-low | aal1 | urn:cre8eve:rakomi:aal1 | magic_link, email_otp |
2 | AAL2 | Substantial | aal2 | urn:cre8eve:rakomi:aal2 | password, totp, passkey-UV+UP |
3 (reserved) | AAL3 | — | (reserved — aal3 is never issued) | urn:cre8eve:rakomi:aal3 | (roadmap — no AAL3 step-up method exists today) |
3(reserved AAL3) is a separate concept from eIDAS “High” assurance. eIDAS Level of Assurance (eidas_low/eidas_substantial/eidas_high) is issued today — independently of the AAL tier system above — whenever EUDI Wallet login succeeds and the verifier is configured for this environment. It does not require reaching “AAL3”; there is no code path today that ever stamps a bareaal3acr value. See OIDC provider guide —acr_values_supportedfor the full, environment-dependent eIDAS vocabulary.
AAL1 step-up tokens MUST NOT satisfy gates that require AAL2. Today all step-up consumers accept any valid token; AAL-aware gates are on the roadmap. Server-side enforcement is via the
aalclaim on the issued token, which the SDK’sverifyStepUpTokenreads for you.
NIST-strict reading would put
passwordat AAL1 (single-factor knowledge). Rakomi treats password as AAL2 by industry-aligned convention — a documented divergence, to be revisited when risk-based auth data is available.
RFC 9470 step-up challenge protocol
Section titled “RFC 9470 step-up challenge protocol”When a route protected by requireStepUpIfMfaEnabled rejects a request for
lack of a step-up token, the response now also carries an RFC 9470 standard
header alongside the legacy JSON body discriminator:
HTTP/1.1 401 UnauthorizedContent-Type: application/jsonWWW-Authenticate: Bearer error="insufficient_user_authentication", acr_values="urn:cre8eve:rakomi:aal2 urn:cre8eve:rakomi:aal1"
{ "error": { "code": "account_linking/mfa_required", "message": "MFA verification required before linking a new identity", "details": { "next_action": "verify_mfa", "mfa_challenge_token": "mfa_step_up_required", "available_methods": ["password", "passkey", "email_otp", "magic_link"] } }}RFC-compliant clients (future enterprise integrations, EUDI relying
parties) can interop on the header; SDK consumers continue to use the JSON
body discriminator (next_action + available_methods).
How to use
Section titled “How to use”@rakomi/node 0.12.0+
Section titled “@rakomi/node 0.12.0+”import { MfaStepUpRequiredError } from "@rakomi/node";
try { await rakomi.link.initiate("google", { userToken });} catch (err) { if (err instanceof MfaStepUpRequiredError) { // Server tells us which methods this user can satisfy. Order is a hint. const methods = err.availableMethods ?? ["password"]; const choice = methods[0]; // …route the user to the corresponding /v1/auth/step-up/<method> flow… // After verify, retry link.initiate() with the issued step_up_token in // the X-Step-Up-Token header. }}Direct API
Section titled “Direct API”POST /v1/auth/step-up/email-otp/initiateAuthorization: Bearer <user-jwt>X-API-Key: <tenant-api-key>Content-Type: application/json
{ "action_hint": "Confirm linking your Google account" }
→ 202 { "status": "sent", "email_masked": "b***@***.com" }POST /v1/auth/step-up/email-otp/verifyAuthorization: Bearer <user-jwt>X-API-Key: <tenant-api-key>Content-Type: application/json
{ "otp": "123456" }
→ 200 { "step_up_token": "...", "token_type": "StepUp", "expires_in": 300 }Then attach the token to the gated request:
POST /v1/users/me/link/googleAuthorization: Bearer <user-jwt>X-Step-Up-Token: <step_up_token>…Audit events
Section titled “Audit events”Every step-up attempt emits one event from this family:
auth.step_up_magic_link_initiated— magic-link initiateauth.step_up_email_otp_initiated— email-OTP initiateauth.step_up_passkey_options_issued— passkey optionsauth.step_up_issued— verify success (any method)auth.step_up_failed— verify failure (any method)
Metadata shape: { step_up_method, aal, latency_ms?, ip_hash, ua_family, [reason] }. The metadata object is intentionally open-shaped (jsonb) so
a future risk engine can attach risk_score / risk_decision without
a schema migration.
Security posture
Section titled “Security posture”- Phishing resistance: distinct email templates from login flows. Subject
- body explicitly state “sensitive action” + “do NOT click if you did not request this”.
- Constant-time compares: token / OTP lookup runs the full hash compare even on structurally invalid input — no timing oracle for “token-was-issued” vs “format-invalid”.
- Session binding: the verify request’s session JWT MUST match the
session that initiated; mismatch → 401 with the same generic
invalid_or_expired_tokencode. - Single-use enforcement:
consumed_atflip is atomic. - Cross-tenant isolation: every challenge / token is
tenant_id-scoped; cross-tenant assertion replay rejected (404, never 403). - AAL2 enforcement on passkey: UP=true AND UV=true both required —
UV=false is rejected with
passkey/step_up_required, NEVER issued at AAL1. - No SMS: SMS-based step-up is excluded by design (SIM-swap, SS7).
See also
Section titled “See also”- SDK errors reference —
MfaStepUpRequiredError,MfaStepUpUnavailableError. - Account linking — the primary gated consumer of step-up tokens today.
- Passkeys — registration + authentication ceremony for the passkey method.