Password Reset

Self-service flow via emailed token.

Files

  • Schema: src/lib/db/schema/password-resets.ts (token PK, user_id, expires_at, used_at).
  • Request endpoint: POST /api/auth/forgot-password.
  • Complete endpoint: POST /api/auth/reset-password.
  • UIs: /forgot-password, /reset-password.

Flow

  1. User visits /forgot-password, submits email.
  2. Server POST /api/auth/forgot-password:
    • Looks up user by email.
    • Always returns { ok: true } — does not reveal whether the email exists.
    • If user exists: inserts password_resets row with a 48-char token, 1-hour expiry. Emails the reset link via the app's own sendEmail() using SYSTEM_FROM_EMAIL.
  3. User clicks link → /reset-password?token=<token>.
  4. User submits new password (min 8 chars).
  5. Server POST /api/auth/reset-password { token, password }:
    • Looks up token — rejects if unknown, expired, or used.
    • Hashes password with bcrypt cost 12.
    • Updates users.password_hash and bumps users.session_version.
    • Revokes every user_sessions row and every unrevoked trusted device.
    • Revokes the account's OAuth refresh tokens and the api_keys rows they minted, then drops the in-memory session cache.
    • Marks token used_at.
    • Writes audit log (user.password_reset) to every org the user belongs to.
  6. UI redirects to /login.

Security

  • 1-hour TTL — much shorter than email verification.
  • Single-use via used_at.
  • 48-char random token (~276 bits).
  • Enumeration-resistant: endpoint always returns success regardless of email existence.
  • Reset is treated as a compromise response and cuts every door at once: session_version (rejects older JWTs on the next request), user_sessions (so the Devices panel agrees rather than listing revoked sessions as active), trusted-device cookies (which skip the emailed login code), and OAuth grants — a browser-login grant consented during the compromise holds a 30-day refresh token that keeps minting live sok_live_* keys, and nothing else reaches it. The OAuth sweep runs last, after the cache eviction and the audit, so a failure there cannot discard the rest of the response; it answers 500 with a message naming what was left undone.

Env

  • SYSTEM_FROM_EMAIL — verified SES identity for the reset email.