Menu

Developer documentation

Step-up reverification

/docs/reverification

Status: shipped 2026-08-04.

Signet's sensitive self-service routes already require a session younger than [session] fresh_age (86,400 seconds by default). Step-up reverification lets a stale but still live session prove a real factor and satisfy that same gate without falsifying the session's public createdAt or extending its expiry.

When a sensitive route returns HTTP 403 SESSION_NOT_FRESH, inspect the session's current options:

const status = await fetch("https://auth.example.com/api/auth/reverify", {
  credentials: "include",
}).then((response) => response.json())

// {
//   fresh, verifiedAt, freshUntil, reverificationId,
//   level, requiredLevel, strategies
// }

freshUntil is the earlier of verifiedAt + fresh_age and the session's existing expiry. A reverification never lengthens the session. reverificationId is null for the original sign-in freshness and rotates to a new random value after each in-place reverification.

Password

For an account without configured MFA:

const result = await fetch("https://auth.example.com/api/auth/reverify/password", {
  method: "POST",
  credentials: "include",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ password }),
}).then(async (response) => ({ response, body: await response.json() }))

A correct password returns {status:true,reverification:{id,level,verifiedAt,freshUntil}}; retry the sensitive request with the same cookie. This route is deliberately distinct from better-auth's POST /verify-password: that compatibility route itself requires a fresh session and is not a stale-session escape hatch.

If verified two-factor is configured, password step-up returns HTTP 403 SECOND_FACTOR_REQUIRED. Signet requires the strongest available level instead of silently downgrading an MFA account to password-only proof.

TOTP, email OTP, and backup codes

The existing authenticated two-factor verification routes are also the step-up wire:

  • TOTP: POST /two-factor/verify-totp with {code}.
  • Email OTP: first POST /two-factor/send-otp, then POST /two-factor/verify-otp with {code}. Email OTP is advertised only when a delivery channel exists.
  • Backup code: POST /two-factor/verify-backup-code with {code}. A successful code remains single-use.

Each successful authenticated factor marks only the current session second_factor; it does not rotate the cookie or change the existing better-auth response body. Sign-in challenge behavior is unchanged.

Passkeys and other sign-in methods

For a non-MFA account with a registered passkey, GET /reverify advertises passkey. Drive the existing /passkey/generate-authenticate-options and /passkey/verify-authentication ceremony. That compatibility flow is an account switch, not an in-place step-up: it mints a new session for the user who proved the passkey. A session-bound challenge is only a request receipt. If the authenticator presents another resident credential, the new session belongs to that other account. Retry with the returned cookie. Do not describe this path as confirming the current session.

A session-bound registration challenge presented to verify-authentication is a typed refusal and burns the challenge; it does not switch accounts.

An account with neither a password, passkey, nor configured second factor receives an empty strategy list for in-place step-up. Re-authenticate through its ordinary social/SSO flow to create a fresh session. Signet does not invent a weaker fallback factor.

Guessing and replay boundary

Five failed password or TOTP submissions inside ten minutes lock step-up on that session for ten minutes. The counter is stored on the session row and updated with guarded database writes, so multiple Signet processes share one budget. Delivered OTP has its existing five-attempt budget per single-use code; backup codes are high-entropy and single-use. All routes retain the ordinary request limiter. Password failures also retain the account lockout policy; step-up is not a route around sign-in protection. Successful verification clears session attempt state. retry-after accompanies REVERIFICATION_LOCKED.

The receipt ID is reusable throughout the freshness window; Signet's own sensitive routes require freshness, not one-receipt-per-action consumption. An application which needs payment-style dynamic linking can store reverificationId with the amount/payee and reject reuse itself. Do not describe the default as one-time transaction authorization.

Enter to open · Esc to close