Files
HealthLog/prisma/migrations/0202_v123_totp_laststep/migration.sql
T
Marc-André Bombeck b446a66753 feat(auth): add TOTP second factor and recovery codes
Add the TOTP enroll/confirm/disable surface and Argon2id-hashed recovery
codes as the v1.23 second factor.

- totp.ts wraps otpauth (zero-dep) pinned to the interop floor (SHA-1, 6
  digits, 30 s) with a +/-1 drift window. Replay protection persists the last
  accepted time-step per account (User.totpLastStep, migration
  0202_v123_totp_laststep) and rejects any step that is not strictly newer,
  so a captured code dies the instant it is accepted once.
- recovery-codes.ts issues ten single-use, high-entropy codes, Argon2id-
  hashed at rest, shown once; regeneration replaces the entire prior set in a
  transaction and verification burns the matched code with a guarded update.
- The TOTP secret is stored AES-256-GCM encrypted (already registered in the
  encrypted-columns registry + rotation script); decrypt is fail-closed.
- Routes: setup (pending secret), confirm (activate + issue recovery codes),
  disable (step-up + current factor), recovery-codes/regenerate (step-up).
  Setup/confirm are cookie-only; disable/regenerate add requireFreshMfa.
- scripts/disable-mfa.ts is the operator escape hatch for a locked-out
  self-hoster, audited, run via pnpm dlx tsx.
2026-06-27 19:56:16 +02:00

12 lines
616 B
SQL

-- v1.23 — TOTP replay guard.
--
-- Persists the last accepted time-step counter per account so a verified
-- code cannot be replayed within its 30-second validity window. The verify
-- path resolves the submitted code to its RFC 6238 step (`floor(now /
-- period)` adjusted by the ±1 drift delta) and rejects any step that is not
-- strictly greater than this column. Additive, nullable — existing rows
-- (no TOTP enrolled) need no backfill, and a freshly confirmed factor starts
-- with NULL (the first verify simply records its step).
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "totp_last_step" BIGINT;