Require step-up before sensitive actions
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/2faandPOST /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
- Call the protected action, for example
POST /identity/manage/info. Without a ReAuth proof, the response is401or403. - Send
GET /identity/manage/authMethodsto see which proofs the user has. The response is{ "password", "authenticator", "recoveryCodes", "passkeys", "passkeyCount" }. - Collect exactly one proof:
{ "password": "..." }{ "twoFactorCode": "..." }{ "twoFactorRecoveryCode": "..." }{ "credentialJson": "..." }from a passkey. See Use a passkey as the proof.
- Send the proof to
POST /identity/confirmIdentitywith the CSRF header. - Check the response:
200 { "reauthToken" }with aSet-Cookieheader forAuthEndpoints.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.
- 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
- Send
POST /identity/confirmIdentity/passkeyOptionswith the CSRF header. The options are for the signed-in user. - Call
navigator.credentials.get. - 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();