Overview
Add TOTP-based two-factor authentication (RFC 6238) so users can protect their accounts with an authenticator app (Google Authenticator, Authy, 1Password, etc.).
No SMS or third-party auth services — TOTP is computed client-side by the authenticator app and verified server-side with a shared secret.
Scope
This is the tracking issue. Sub-issues cover each layer:
Design decisions
MFA pending state: After a successful password check, if 2FA is enabled the server returns 202 Accepted with a short-lived signed JWT (type: 'mfa_pending', 5 min expiry, not usable as an access token). The client then prompts for a TOTP code and posts it to /api/auth/totp. This avoids storing any per-login state in the DB.
Backup codes: 8 single-use codes generated at setup time, shown once, stored as bcrypt hashes in the DB. Usable in place of a TOTP code at both the login step and the disable-2FA step.
Password reset: Disables 2FA and clears totp_secret when key_salt is rotated (password reset). A user who has lost both their device and backup codes can recover via password reset.
Libraries (server-side):
otpauth — TOTP generation + verification, pure JS, no native deps
qrcode — generate QR code as data URL for setup UI
Out of scope (for now)
- SMS-based 2FA
- WebAuthn / hardware keys
- Per-device trusted sessions ("remember this device for 30 days")
Overview
Add TOTP-based two-factor authentication (RFC 6238) so users can protect their accounts with an authenticator app (Google Authenticator, Authy, 1Password, etc.).
No SMS or third-party auth services — TOTP is computed client-side by the authenticator app and verified server-side with a shared secret.
Scope
This is the tracking issue. Sub-issues cover each layer:
POST /api/auth/totp)Design decisions
MFA pending state: After a successful password check, if 2FA is enabled the server returns
202 Acceptedwith a short-lived signed JWT (type: 'mfa_pending', 5 min expiry, not usable as an access token). The client then prompts for a TOTP code and posts it to/api/auth/totp. This avoids storing any per-login state in the DB.Backup codes: 8 single-use codes generated at setup time, shown once, stored as bcrypt hashes in the DB. Usable in place of a TOTP code at both the login step and the disable-2FA step.
Password reset: Disables 2FA and clears
totp_secretwhenkey_saltis rotated (password reset). A user who has lost both their device and backup codes can recover via password reset.Libraries (server-side):
otpauth— TOTP generation + verification, pure JS, no native depsqrcode— generate QR code as data URL for setup UIOut of scope (for now)