Changelog
Guides

Require step-up before sensitive actions

Ask a signed-in user to prove their identity again before a sensitive change, then retry the change.

Some changes need a fresh proof of identity (ReAuth) even when the user is signed in. Browser clients receive the AuthEndpoints.ReAuth cookie. API clients send the reauthToken from the confirm response in the X-AuthEndpoints-Reauth header. The cookie and the token share one lifetime: 5 minutes by default (ReAuth.Lifetime).

These actions require ReAuth:

  • POST /identity/manage/2fa and POST /identity/manage/info.
  • Passkey creationOptions, add, rename, and delete.
  • External OAuth unlink.
  • Host endpoints that call .RequireReauth().

The steps assume a signed-in cookie session. Both POST /identity/confirmIdentity and POST /identity/confirmIdentity/passkeyOptions require a CSRF token, because the facade maps them with management. Get the token from GET /identity/csrfToken and send it in the RequestVerificationToken header. A missing token returns 400 with the body Invalid or missing CSRF token.

Complete step-up

  1. Call the protected action, for example POST /identity/manage/info. Without a ReAuth proof, the response is 401 or 403.
  2. Send GET /identity/manage/authMethods to see which proofs the user has. The response is { "password", "authenticator", "recoveryCodes", "passkeys", "passkeyCount" }.
  3. Collect exactly one proof:
    • { "password": "..." }
    • { "twoFactorCode": "..." }
    • { "twoFactorRecoveryCode": "..." }
    • { "credentialJson": "..." } from a passkey. See Use a passkey as the proof.
  4. Send the proof to POST /identity/confirmIdentity with the CSRF header.
  5. Check the response:
    • 200 { "reauthToken" } with a Set-Cookie header for AuthEndpoints.ReAuth: step-up succeeded.
    • 400 "Provide exactly one of Password, TwoFactorCode, TwoFactorRecoveryCode, or CredentialJson.": the body had zero proofs or more than one.
    • 401: the proof was wrong.
  6. Retry the protected action. A browser sends the ReAuth cookie automatically. An API client adds the X-AuthEndpoints-Reauth: <reauthToken> header.

Use a passkey as the proof

  1. Send POST /identity/confirmIdentity/passkeyOptions with the CSRF header. The options are for the signed-in user.
  2. Call navigator.credentials.get.
  3. Send the credential to POST /identity/confirmIdentity.
const headers = { 'Content-Type': 'application/json', 'RequestVerificationToken': csrfToken };

const options = await fetch('/identity/confirmIdentity/passkeyOptions', {
  method: 'POST', credentials: 'include', headers
}).then(r => r.json());

const credential = await navigator.credentials.get({
  publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(options)
});

await fetch('/identity/confirmIdentity', {
  method: 'POST', credentials: 'include', headers,
  body: JSON.stringify({ credentialJson: JSON.stringify(credential) })
});

A passkey that belongs to a different user returns 401.

Protect your own endpoint

Call .RequireReauth() on the endpoint. The facade registers the ReAuth schemes. A composed host calls AddCookieAuthEndpoints or AddBearerAuthEndpoints.

app.MapPost("/billing/update", handler)
    .RequireAuthorization()
    .RequireReauth();