# Blueprint: Split the password "change" and "reset" pathways

## Created: 2026-06-01
## Type: UX + security redesign (authenticated in-app change vs. logged-out recovery)
## Worktree: scheduled-changes-phantom-row-fix (reused, user-confirmed)

---

## Context

A logged-in user who tries to set/change their password from Settings → Security is dropped into
the chrome-less logged-out "reset password" UI and signed out at the end. Wrong model. Split into:
- **Change** (logged-in, in-app, session preserved) — for users with a password.
- **Set a password** (logged-in, in-app) — for OAuth-primary users with no password, gated by
  `oauth_reauth` step-up (re-confirm through their provider).
- **Reset / Recover** (logged-out, standalone) — unchanged mechanics, reframed copy.

The in-app pieces already exist but are unwired (`SetInitialPasswordModal` mounted nowhere) because
`oauth_reauth` step-up never existed. Building `oauth_reauth` is the linchpin.

---

## Implementation checklist

### 1. `oauth_reauth` step-up (backend)
- [ ] `lib/auth/stepUpReauthState.ts` (NEW) — HMAC-signed state token (userId+intent+provider+nonce+exp).
- [ ] `lib/auth/elevation.ts` — add `getElevationStatus(userId)` → `{ elevated, expiresAt, error }`.
- [ ] `app/api/settings/security/oauth/reauth/start/route.ts` (NEW, withAuth) — initiate provider
      re-auth with `prompt=login`; reject email-primary with 400; set signed state cookie; return `{ url }`.
- [ ] `app/api/auth/callback/route.ts` — `type === 'stepup_reauth'` branch: verify signed state +
      same-user; `grantElevation({ challengeMethod:'oauth_reauth', ttlMinutes:5 })`; redirect
      `/settings/security?stepup=ok&intent=…` or `?stepup=identity_mismatch` (mismatch → signOut).
- [ ] `app/api/settings/security/step-up/route.ts` — add `GET` → `{ elevated, expiresAt }`.

### 2. Wire the in-app OAuth set-password (frontend)
- [ ] `app/settings/security/SecurityClient.tsx` — read `?stepup=ok&intent` + `identity_mismatch`;
      wrap requireStepUp to inject `reauthProvider`; thread primaryProvider/email/autoOpen to SignInSection.
- [ ] `components/security/SignInSection.tsx` — add `primaryProvider`/`email`/`autoOpenSetPassword`;
      include `canSetPassword` in section-content gate; pass through to PasswordCard.
- [ ] `components/security/PasswordCard.tsx` — remove `/auth/forgot-password` link + "2FA first" copy;
      render "Set a password" (OAuth no-pw) / "Change password" / "Change backup password"; mount
      SetInitialPasswordModal + ChangePasswordModal; auto-open on return.
- [ ] `components/security/SetInitialPasswordModal.tsx` — gate counts oauth_reauth; submit ensures
      fresh grant (probe GET; inline step-up if MFA, else oauth_reauth redirect intent=set_password).
- [ ] `hooks/useStepUp.tsx` + `components/security/StepUpModal.tsx` — probe-first; add `oauth_reauth`
      redirect method offered for OAuth-primary via `reauthProvider`/`reauthIntent` opts.
- [ ] `lib/auth/stepUpClient.ts` (NEW) — client helpers `startOauthReauth(intent)`, `probeElevation()`.

### 3. Change-password hardening + fallback
- [ ] `components/security/ChangePasswordModal.tsx` — ensure elevation before submit (self-mint via
      typed current password when no MFA, else inline TOTP step-up); add "Forgot current password?"
      recovery-email fallback; copy "you'll stay signed in on this device".
- [ ] `app/api/settings/security/change-password/route.ts` — gate on password existence
      (signInWithPassword) instead of provider-name heuristic so backup-password change works for
      google AND azure OAuth-primary users.

### 4. Logged-out recovery copy
- [ ] `app/auth/forgot-password/page.tsx` — reframe as recovery.
- [ ] `app/auth/reset-password/page.tsx` — recovery wording; success → `/login?reason=password_updated`.

### 5. Overview payload
- [ ] Add `email` to SecurityOverview (route + server page + type) for the forgot-password fallback.

### 6. Docs
- [ ] `app/docs/account/sign-in-methods/page.tsx` + `lib/docsIndex.ts` keywords.

### 7. (No migration) — `admin_elevation_grants.challenge_method` already permits `'oauth_reauth'`.

---

## Security rationale
Adding a password creates a new persistent credential — a backdoor if a session were hijacked — so a
fresh proof (OAuth `prompt=login`) is the right bar. The signed-state + identity-match check in the
callback prevents a different provider account from minting a grant for the current user; on mismatch
we signOut the displaced session.

## Adversarial audit after build: (a) security/auth (oauth_reauth grant + identity-match + no-bypass),
(b) UX/journey (never logged out / never loses shell, desktop + mobile), (c) CLAUDE.md/regression.
