Skip to main content

MFA with TOTP, recovery codes, and trusted devices

RFC 6238 TOTP, single-use recovery codes, per-device 30-day trust cookie. WebAuthn deferred; SMS / email second factors never (NIST-deprecated for AAL2).

Multi-factor authentication for the dashboard, opt-in per-user via Settings, Security. TOTP secret is AES-GCM encrypted at rest (key derived from Z4J_SECRET via HKDF). Recovery codes are argon2id-hashed, single-use, fan-out across a constant-time scan to defeat timing oracles. Trusted-device cookie is HttpOnly + Secure + SameSite=Strict with a server-side hash so a stolen browser cookie alone does not bypass MFA on a new IP. Sensitive actions (password change, API key mint, project delete) re-prompt for MFA via a GitHub-style 60-minute sudo window. Operators with lost authenticators have the z4j reset-mfa CLI escape hatch; every reset writes a tamper-evident audit row attributed to the OS user.

Ships with

  • TOTP RFC 6238 with universal authenticator-app compatibility (SHA-1, 6 digits, 30s period)
  • 10 recovery codes per user (configurable 5..50); argon2id-hashed, single-use, atomic consume guard
  • Trusted-device cookie scoped to 30 days; password change revokes every trust row
  • Sensitive-action gate (`require_fresh_mfa`) returns 403 mfa_reverify_required after 60 min idle
  • 9 routes under /auth/mfa: enroll-start, enroll-complete, verify, disable, recovery-codes/regenerate, status, trusted-devices list/revoke/rename
  • z4j reset-mfa CLI escape hatch for the lost-phone-AND-lost-codes case; audit row attributed to OS user

Highlights

  • AES-GCM encryption with AAD binding the user id; an operator who copy-pastes one user's secret onto another row hits InvalidTag on decrypt
  • Constant-time scan over recovery codes; argon2id burns one dummy cycle on the empty-list path to defeat timing oracles
  • WebAuthn / passkeys deferred to a later minor (additive); SMS + email second factors are NEVER coming (NIST SP 800-63B deprecates them for AAL2)
  • 8 audit actions chained: user.mfa_enrolled / disabled / reset_by_admin / verified / recovery_code_used / recovery_codes_regenerated / trusted_device_added / trusted_device_revoked
Related

More capabilities