Built-in MFA

How the automatic MFA flow works when the IAM service detects anomalies during token rotation, including the magic link verification and code submission routes.

The built-in MFA flow is triggered automatically by the IAM anomaly detection system. When a user's session raises a flag during token rotation, the IAM service sends a verification email and returns HTTP 202 instead of rotating the tokens. The gateway ships two ready-to-use routes that handle the verification without any custom server code.


When MFA is triggered

The IAM service evaluates nine anomaly checks on every token rotation request. Any of the following conditions can trigger an MFA challenge:

ConditionDescription
New deviceThe canary_id cookie does not match the stored fingerprint
Idle sessionMore than 24 hours since the last request
Too many sessionsActive session count exceeds the configured limit
IP range changeCurrent IP is not in the same subnet as the stored IP
Elevated risk scoreSuspicious activity score exceeds 25% of the ban threshold
Proxy or hostingRequest originates from proxy or hosting infrastructure the user has not previously allowed
Device fingerprint driftBrowser, OS, or device type does not match the stored values

When any check fails, the IAM service generates a magic link JWT and a 7-digit verification code, sends both to the user's email, and returns { mfa: true, message: "..." } with status 202.

See IAM Anomaly Detection for the full list of checks and how the bypass window works after successful verification.


Route registration

The built-in MFA routes are registered by magicLinksRouter.

import { createRouter } from 'h3'
import { magicLinksRouter } from 'auth-h3client/v1'

const router = createRouter()
magicLinksRouter(router, 'api')

If you use the Nuxt module, these routes are registered automatically by defineAuthConfiguration in your Nitro plugin.

The prefix parameter prepends a path segment to all routes. With 'api' as the prefix:

MethodPathHandlerPurpose
GET/api/auth/verify-mfaverifyTempMfaLinkValidates the magic link
POST/api/auth/verify-mfasendMfaCodeHandlerSubmits the 7-digit code

When the user clicks the magic link in their email, the bounce route redirects them to your frontend verification page with query parameters (token, random, reason, visitor). The frontend code needs to sends a GET request to /api/auth/verify-mfa with those parameters, and all cookies identifiers.

The handler validates the request in this order:

  1. Sets Cache-Control: no-store to prevent caching.
  2. Checks that the canary_id cookie and token query parameter are present.
  3. Proxies the request to the IAM service for link signature verification.

On success, the handler returns the verification result from the IAM service:

{
  "ok": true,
  "date": "2026-04-12T10:00:00.000Z",
  "data": {
    "reason": "MAGIC_LINK_MFA_CHECKS",
    "link": "MFA Code"
  }
}

The useMagicLink composable handles this process automatically and reads the data fields. The frontend then can use link to confirm the flow type and renders the code input form.

If the link is invalid or expired, the handler returns a 404 error response.


POST: code submission

After the user enters the 7-digit code, the frontend submits it as a POST request. The route applies the following middleware before the handler runs:

  1. Link verification: the GET handler runs first as middleware to re-validate the magic link parameters.
  2. CSRF verification: validates the X-CSRF-Token header against the __Host-csrf cookie.
  3. Content-Type: rejects requests without Content-Type: application/json.
  4. Body limit: rejects request bodies larger than 1 KB.

The handler then:

  1. Reads the code field from the request body.
  2. Proxies the code and the magic link parameters to the IAM /auth/verify-mfa endpoint.
  3. On success, the IAM service verifies the code, rotates both tokens, and returns the new credentials.
  4. The handler sets the __Secure-a and a-iat cookies with the new access token and forwards the Set-Cookie headers from the IAM response (containing the new session cookie).

The response depends on the Accept header:

  • Accept: application/json: returns { "ok": true, "redirectTo": "/dashboard" } where the redirect URL comes from the onSuccessRedirect configuration.
  • Otherwise: responds with HTTP 303 redirect to onSuccessRedirect.

Error responses

IAM statusGateway responseMeaning
200200 or 303 redirectCode verified, tokens rotated
400400Invalid or expired code
401401Invalid or expired code
403403Missing session cookies or banned user
429429 with Retry-AfterRate limited
500500Server error

Deduplication

The code submission handler is wrapped with defineDeduplicatedEventHandler. If two identical requests arrive simultaneously for the same session, only one is processed. The second request waits for the result of the first. This prevents race conditions from double-submit on the frontend.


What happens after verification

After successful MFA verification, the IAM service updates users.last_mfa_at. This timestamp creates a bypass window (configurable on the IAM service) during which certain anomaly checks are relaxed. For example, the session count check is skipped for 5 minutes after MFA completion, allowing the user to open multiple tabs without triggering another challenge.

See IAM Anomaly Detection for how the bypass window affects each check.

Logo