Found by QA run #10663 driving identity-auth.two-factor-backup-codes at framework 79ebb37. Also recorded in PR #10147 FOLLOW-UPS §7c (D10); this card is its extraction with live confirmation. Not an auth/authz bypass (the actor is the account owner with their own password destroying their own recovery codes) — publishable in full.
Symptom
sys_user.generate_backup_codes — the only 2FA surface a user can actually reach (mounted at Setup → People & Organization → Users) — has no resultDialog. It toasts "New backup codes generated — save them somewhere safe." (sys-user.object.ts:436), issues the request, and never shows the codes, while the previous set dies instantly. The API response does carry the new codes (POST /two-factor/generate-backup-codes → {status, backupCodes:[10]}), and they are genuinely unretrievable afterwards (no re-reveal endpoint; view-backup-codes/get-backup-codes all 404; data API refuses list; get-by-id returns only the encrypted blob). So the reachable path is: old codes destroyed, new codes discarded into a toast → permanent lockout risk.
The resultDialog-bearing variant sys_two_factor.regenerate_backup_codes (sys-two-factor.object.ts:104) exists but is mounted in no app (account.app.ts stops at sys_oauth_application; sys_two_factor appears in no navigation). The enrollment action sys_user.enable_two_factor has the same no-QR shape.
Suggested fix
Add a one-shot resultDialog ({path:'backupCodes', format:'code-list'}) to sys_user.generate_backup_codes (and the QR to sys_user.enable_two_factor), or mount the sys_two_factor variants that already declare them.
Passing clauses
The cryptographic/behavioral core is sound: backup codes are a real second factor, single-use (same code refused twice, positive control confirms the set isn't globally dead), regeneration invalidates the prior set wholesale, regeneration is password- + session-gated (wrong password → 400 INVALID_PASSWORD strict no-op; anonymous → 401), and codes are encrypted at rest (334-char hex blob, none of the 10 plaintext codes present).
Docs-accuracy note (same area)
sys_two_factor.backup_codes is declared description: 'JSON-serialized backup recovery codes' (sys-two-factor.object.ts:163) but the column holds a single encrypted hex blob — no JSON. One-line description fix.
QA-source: #10663 · identity-auth.two-factor-backup-codes · clause 5 (observe-and-flag lockout path)
Found by QA run #10663 driving
identity-auth.two-factor-backup-codesat framework79ebb37. Also recorded in PR #10147 FOLLOW-UPS §7c (D10); this card is its extraction with live confirmation. Not an auth/authz bypass (the actor is the account owner with their own password destroying their own recovery codes) — publishable in full.Symptom
sys_user.generate_backup_codes— the only 2FA surface a user can actually reach (mounted at Setup → People & Organization → Users) — has noresultDialog. It toasts "New backup codes generated — save them somewhere safe." (sys-user.object.ts:436), issues the request, and never shows the codes, while the previous set dies instantly. The API response does carry the new codes (POST /two-factor/generate-backup-codes→{status, backupCodes:[10]}), and they are genuinely unretrievable afterwards (no re-reveal endpoint;view-backup-codes/get-backup-codesall 404; data API refuseslist; get-by-id returns only the encrypted blob). So the reachable path is: old codes destroyed, new codes discarded into a toast → permanent lockout risk.The
resultDialog-bearing variantsys_two_factor.regenerate_backup_codes(sys-two-factor.object.ts:104) exists but is mounted in no app (account.app.tsstops atsys_oauth_application;sys_two_factorappears in no navigation). The enrollment actionsys_user.enable_two_factorhas the same no-QR shape.Suggested fix
Add a one-shot
resultDialog({path:'backupCodes', format:'code-list'}) tosys_user.generate_backup_codes(and the QR tosys_user.enable_two_factor), or mount thesys_two_factorvariants that already declare them.Passing clauses
The cryptographic/behavioral core is sound: backup codes are a real second factor, single-use (same code refused twice, positive control confirms the set isn't globally dead), regeneration invalidates the prior set wholesale, regeneration is password- + session-gated (wrong password → 400 INVALID_PASSWORD strict no-op; anonymous → 401), and codes are encrypted at rest (334-char hex blob, none of the 10 plaintext codes present).
Docs-accuracy note (same area)
sys_two_factor.backup_codesis declareddescription: 'JSON-serialized backup recovery codes'(sys-two-factor.object.ts:163) but the column holds a single encrypted hex blob — no JSON. One-line description fix.QA-source: #10663 · identity-auth.two-factor-backup-codes · clause 5 (observe-and-flag lockout path)