#1245 MFA Backend — Execution Plan
For agentic workers: execute via superpowers:subagent-driven-development. The authoritative spec is the on-branch checklist
docs/proposals/1245-mfa-backend-implementation.md(§0–§7) + proposaldocs/proposals/1245-mfa-backend.md. This file only defines the task grouping, commit boundaries, and acceptance so the checklist isn't duplicated.
Goal: Fill in the MFA backend business logic behind the landed scaffold (7 service methods currently throw NotImplementedException) + enforcement guard + rate-limit/lockout + audit + tests. Frontend is a separate follow-up (1245-mfa-frontend-implementation.md).
Branch: feat/1245-mfa-totp-backend (now merged up to current dev, 0 behind).
Scaffold already present (do NOT re-create):
- Entities:
User.{twoFactorSecretEncrypted(Buffer,select:false), twoFactorEnabled, twoFactorEnrolledAt, twoFactorLastUsedAt, twoFactorDisabledAt, mfaRequiredSince},MfaRecoveryCode{userId, codeHash, usedAt},MfaChallenge{userId, tokenHash(unique), attempts, consumedAt, expiresAt, ipAddress}(auth.entities.ts). - Endpoints in
auth.controller.ts+ DTOs inauth.dto.ts(MfaSetupResponse/MfaStatusResponse/login-mfa DTO etc.). - Service stubs in
auth.service.ts:issueMfaChallenge,loginMfa,setupMfa,verifyMfa,disableMfa,regenerateRecoveryCodes,getMfaStatus. - Migration
1794000000001-MfaTotpBackend.ts. buildToken(user, amr)+JwtPayload.amralready wired.
Crypto reuse (mandatory): encryptOAuthToken/decryptOAuthToken (api/src/common/oauth-token-crypto.ts, AES-256-GCM, hard-fails on missing MASTER_ENCRYPTION_KEY). Recovery codes: bcrypt cost 10. Challenge token: sha256(token) stored, raw returned. otplib + qrcode for TOTP.
Node 22 for all build/test (source ~/.nvm/nvm.sh && nvm use 22). API tests: sqlite locally (Postgres-only specs skip locally, enforced in CI).
Group 0 — Migration hygiene (no business logic)
- Re-timestamp
1794000000001-MfaTotpBackend.ts→ run last (e.g.1796000000005-MfaTotpBackend.ts, classMfaTotpBackend1796000000005) so it never collides with the three other1794000000001-*migrations or runs before current-dev schema. Update filename + class name +nameproperty. - Confirm
migrations:runapplies cleanly against the local docker DB (the dev-current schema). Verify theusers_2fa_secret_present_when_enabledCHECK + partial unused-recovery-codes index +mfa_challenges.token_hashuniqueness exist after. - Commit:
chore(1245): re-timestamp MFA migration to run last - NOTE (not code): checklist §0 production gate —
SELECT count(*) FROM users WHERE two_factor_enabled=trueon staging/prod must be 0 before this migration runs there, else a pre-step (force-disable + re-enroll email) is required. Flag in the PR; do not attempt against prod.
Group 1 — Crypto + helpers (foundation)
Checklist §1. TOTP secret encrypt/decrypt (store base32 as ciphertext in two_factor_secret_encrypted, .addSelect when reading), recovery-code generation (10× xxxx-xxxx-xxxx, bcrypt-hashed), challenge-token gen (sha256 persisted), re-import otplib generateSecret/generateURI/verify + qrcode. Pure helpers + unit tests (no endpoint wiring).
- Commit:
feat(1245): MFA crypto + recovery-code/challenge helpers
Group 2 — Enrolment service methods
Checklist §2: setupMfa, verifyMfa, disableMfa (403 for MFA_REQUIRED_ROLES when enforcement on), regenerateRecoveryCodes, getMfaStatus (grace-days from mfaRequiredSince ?? createdAt). TDD: happy-path + failure per method (bad TOTP, disable-blocked-for-staff, recovery single-use). Audit verbs (§5) for each.
- Commits:
feat(1245): MFA enrolment service (setup/verify/disable/status/regenerate)(+ test commit if separated)
Group 3 — Two-step login
Checklist §2: issueMfaChallenge (called from login() — already wired), loginMfa (atomic challenge consume UPDATE ... WHERE token_hash=$1 AND consumed_at IS NULL AND expires_at>now() RETURNING *, XOR totp/recovery, attempt-cap burn, buildToken(user,["pwd","otp"|"rc"]), stamp twoFactorLastUsedAt, audit success/failure). TDD incl. expired/consumed challenge, attempt-cap burn, recovery single-use.
- Commit:
feat(1245): two-step MFA login (challenge issue + redeem)
Group 4 — Enforcement guard + rate-limit/lockout
Checklist §3 + §4. New api/src/auth/mfa-enforcement.guard.ts (route-scoped on /ops + /workspace, NOT global APP_GUARD): grace header within window, 403 {error:"mfa_required", enroll_at} past grace, exempt impersonation + non-prod demo + amr already-otp. Config env (MFA_REQUIRED_ROLES, MFA_GRACE_PERIOD_DAYS, MFA_ENFORCE_BLOCK_PATHS) → .env.example. Per-user sliding-window lockout via Redis-backed ThrottlerStorageFactory (NOT in-process); Slack alert on lockout. Guard unit tests (grace header / post-grace 403 / impersonation exemption).
- Commits:
feat(1245): MFA enforcement guard + config,feat(1245): MFA login rate-limit + lockout
Group 5 — E2E bypass gate + cleanup job
Checklist §6 (E2E) + §7. TEST_MFA_BYPASS=true honored only when APP_ENV !== "production" (mirror demo-login gate; no hard-coded bypass tokens). BullMQ scheduled job (stable jobId, active-active) to delete expired mfa_challenges.
- Commits:
test(1245): MFA-bypass login fixtures (non-prod gated),feat(1245): expired-challenge cleanup job
Acceptance (backend slice of the ticket)
- TOTP enrol/verify/disable/recovery + two-step login implemented; all stubs replaced.
- Role enforcement (SA/AM/Expert) with grace-period guard.
- Recovery codes single-use, encrypted, returned once.
audit_logon enrol/disable/recovery-use/verify/login-success/login-failure/regenerate.npm testgreen (Postgres-only specs skip locally, run in CI); migration applies forward.- Live smoke on the local stack: setup → verify → login two-step → disable-blocked-for-staff.
Out of scope (this PR)
Frontend (separate PR per 1245-mfa-frontend-implementation.md); admin force-disable; WebAuthn/SMS; key rotation.