Commit Graph
7 Commits
Author SHA1 Message Date
Marc-André Bombeck 0f2b548138 style: apply prettier 3.9.1 formatting
Mechanical reflow from the prettier 3.9.1 bump; no behaviour change.
2026-06-28 19:26:16 +02:00
Marc-André Bombeck 2c1151dd40 fix(auth): guard the webauthn login challenge kind and require user verification
Mirror the TOTP login path: both the security-key verify and verify-options
routes now reject any challenge whose kind is not "login" with the same generic
401, so a future step-up or non-login challenge can never mint a login session
through these endpoints. Set the registration and authentication ceremonies to
userVerification:"required" to match the server's UV enforcement — a key that
passes the browser but skips UV would otherwise fail server-side.
2026-06-27 22:22:46 +02:00
Marc-André Bombeck c458d955e4 merge: integrate the v1.23 security settings and MFA wave
# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	src/lib/openapi/routes/auth.ts
#	src/lib/query-keys/auth.ts
2026-06-27 20:46:03 +02:00
Marc-André Bombeck 402ea90d45 feat(security): add a TOTP and security-key setup hub
Introduce a dedicated /settings/security section that gathers every
account-protection control in one place: TOTP enrolment (QR plus manual
secret, confirm, recovery codes shown once with copy and download),
WebAuthn security keys as a second factor (register, list with last-used,
rename, step-up-gated removal), recovery-code count and regeneration, and
primary passkey management.

The second-factor WebAuthn path keeps its own credential store, separate
from primary passkeys, and uses non-resident keys so an account can hold
many. Relying-party resolution is shared with the passkey path. A status
endpoint backs the hub; the step-up gate now recognises a registered
security key as an active second factor so a WebAuthn-only account can
still reach the gated actions.
2026-06-27 20:40:36 +02:00
Marc-André Bombeck 8f56ac95d8 fix(auth): harden the second-factor verify and disable surfaces
Reject any challenge that is not a login challenge before minting a session,
cap the disable and confirm request bodies through safeJson, and correct the
recovery-code scan comment to match its early-return-on-match behaviour.
2026-06-27 20:08:10 +02:00
Marc-André Bombeck 6b3d3fd840 feat(auth): branch login through a second-factor challenge
When a password is accepted for an account with a confirmed TOTP factor, the
login route no longer mints a session or token. It creates a single-use,
~5-minute MfaChallenge (the opaque ticket is stored only as a hash) and
returns HTTP 200 with data:null, error:null and meta.mfaRequired + mfaTicket +
methods. The client completes the login at POST /api/auth/mfa/verify with the
ticket + a TOTP or recovery code, receiving the exact same token bundle the
password path issues, with the session stamped second-factor-verified.

- The ticket is claimed once, atomically, only after the factor verifies, so
  two concurrent submissions can mint at most one session; no session ever
  exists before factor two.
- Wrong codes increment the ticket attempt counter and burn it at the cap
  (throttle, not an account lock); the anonymous verify surface is also
  rate-limited per IP.
- meta.mfaRequired is returned only after a valid password and the
  invalid-credentials response is identical regardless of MFA state, so an
  attacker without the password cannot enumerate MFA.
- The session/token tail is extracted to login-response.ts and shared by both
  the password path and the verify path, keeping the transport contract
  identical. OpenAPI registers the verify endpoint, the MFA management
  endpoints, and the mfaRequired login variant.
2026-06-27 19:56:30 +02:00
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