# Step-up re-authentication

> Let passwordless OAuth users confirm sensitive actions with a passkey or a fresh OAuth round-trip instead of a password.

Some actions are sensitive enough to re-check who is at the keyboard: deleting an account, changing the email,
disabling two-factor, or setting a password. Slicekit asks the user to re-authenticate for these, and accepts the
result in place of the normal password prompt. Users who signed up with a password just type it. Users who signed in
with [an OAuth provider](/docs/oauth-provider) have no password, so they re-authenticate with a
[passkey](/docs/two-factor-auth) or a fresh OAuth round-trip instead.

## The proof

A re-authentication produces a short-lived, single-use **proof**: a token stored in the cache for five minutes,
keyed by both the user id and the token so it can never be replayed for another account. The proof is minted by the
re-auth endpoints and consumed by the sensitive operation. It lives in `Slicekit.Core.Auth` (outside the feature
slices) because several features depend on it.

## Re-auth endpoints

All are authenticated, CSRF-protected and rate-limited, and they return `{ reauthToken }`.

- `POST /auth/reauth/passkey/options` and `POST /auth/reauth/passkey` run a WebAuthn assertion for the current user
  (the same machinery as the passkey 2FA flow) and mint a proof.
- `GET /auth/reauth/{provider}/start` and `POST /auth/reauth/{provider}/callback` run an OAuth round-trip, verify the
  returned identity is already linked to the current user, and mint a proof. They reuse the login redirect URI, with
  their own cache namespace and cookie so a re-auth can never be mistaken for a login.

## Password or proof

The sensitive operations (`DELETE /users/me`, `POST /users/me/request-email-change`, `DELETE /users/me/totp`, and
`POST /users/me/change-password`) accept an optional re-auth token next to the password field. Each handler applies
one rule: if the user has a password, verify the password; if not, verify and consume the proof. Change-password
doubles as **set a password** for passwordless users, so an OAuth-only account can add password sign-in after proving
itself. `GET /users/me` exposes a `hasPassword` flag so the UI knows which path to show.

## On the frontend

The `reauth` feature slice provides a `StepUpVerify` component that offers "Verify with passkey" (inline) and
"Continue with `<provider>`". The OAuth option is a full-page redirect in the same tab (not a popup, which providers
break with their Cross-Origin-Opener-Policy): the pending operation is stashed in `sessionStorage`, the OAuth
callback completes the re-auth, and the originating form resumes on return. Password-based forms are unchanged; the
step-up UI appears only when `hasPassword` is false.
