Phase 2: password reset flow

↗ view on GitHub · Claude · 2026-05-15 · 9e2123cd

Adds the Credentials password-reset flow. Entra-only accounts have no
local password and are silently ignored.

Backend:
- Migration 0012_password_reset.sql adds password_reset_token_hash and
  password_reset_expires_at columns to public.users with a partial
  index for token lookup.
- New backend/src/lib/email.ts wraps Resend. RESEND_API_KEY is
  required in production; in development the email body is written
  to the backend log so the operator can complete the flow manually.
- New POST /auth/request-password-reset and POST /auth/reset-password
  routes. The request endpoint always returns 200 regardless of
  whether the email is registered (prevents enumeration). Tokens are
  32 random bytes encoded base64url, hashed SHA-256 before storage
  (raw token never persisted), valid for one hour, single-use.
- Wrong token and expired token return the same 400 message so a
  brute-force attacker cannot discriminate. Weak password returns a
  separate 400 with the minimum-length message.
- Audit events: user.password-reset.request and
  user.password-reset.complete.
- Disabled accounts cannot reset.

Frontend:
- New /forgot-password page submits the email and shows a generic
  "if an account exists" success message.
- New /reset-password page reads ?token=, validates password length
  and confirmation, calls the backend, redirects to /login on
  success.
- "Forgot password?" link added under the Login submit button.
- middleware.ts adds /forgot-password and /reset-password to the
  public allowlist.

End-to-end verification against Postgres 16:
- Unknown email: 200, no email sent.
- Known email: 200, link captured from backend log.
- Wrong token: 400 generic.
- Weak password: 400 with min-length message.
- Valid token + strong password: 200, password updated, reset
  columns cleared.
- Same token re-used: 400 generic (one-shot).
- Old password rejected, new password signs in.
- audit_events recorded both events.

Docs:
- docs/developer/07-auth.md: new Password reset section.
- backend/.env.example: documents RESEND_API_KEY and RESEND_FROM.
- CHANGELOG and Instructions updated.
Repository cpatpa/PIP
Author Claude <noreply@anthropic.com>
Authored
Parents d2a8512b
Stats 11 files changed , +682
Part of Phase 2 - Auth.js v5 + Entra OIDC + password reset

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-9e2123cd.md from inside the repo you want the change in.

⬇ Download capture-commit-9e2123cd.md