Skip to content

Identity: recovery codes status & download #180

@antosubash

Description

@antosubash

Summary

Surface 2FA recovery code state to users and let them download / print their codes safely. Today, recovery codes are generated and shown exactly once — if the user closes the tab, they're locked out the moment they lose their authenticator app.

Why we need this

UserManager.CountRecoveryCodesAsync exists and is the standard way for a user to see "how many recovery codes do I have left". It is currently not exposed anywhere in SimpleModule. Combined with the once-only display behavior of GenerateRecoveryCodesEndpoint, this creates a real user-experience risk:

  • User enables 2FA, sees the codes, thinks "I'll save these later", closes the tab. Now they have 10 unknown codes that are still valid but unrecoverable.
  • User uses some codes over time and has no idea how many remain. The first time they find out is when login fails.
  • User wants to print the codes for a safe deposit box but the only output today is a copy-paste-friendly list rendered in the browser — no print stylesheet, no download.

The fix is small, additive, and aligned with how every mature Identity-based product (GitHub, Microsoft, Google) handles this.

How the user will use it

On Manage/TwoFactorAuthentication (the existing 2FA management page):

  1. Status row: "Recovery codes: 7 of 10 remaining" with a warning style when ≤3 remain. Calls CountRecoveryCodesAsync.
  2. "Download codes" button on the page that displays codes after generation — emits a plain-text .txt file (simplemodule-recovery-codes.txt) containing the codes plus a header with the user's email and a generated-on timestamp.
  3. "Print codes" button that triggers window.print() against a print-styled view (clean black-on-white list, no chrome).
  4. "Generate new codes" CTA when remaining ≤3, linking to the existing GenerateRecoveryCodesEndpoint. Make clear that doing so invalidates all existing codes.
  5. After regeneration, show the same download / print buttons.

Important: we do not need to (and should not) re-display previously generated codes. Identity stores them hashed, like passwords — they cannot be recovered. The only paths are "download at generation time" or "regenerate, which invalidates the old set". The UI must communicate this clearly so users don't expect retrieval.

Implementation notes

  • Add a recoveryCodesRemaining field to the props returned by Manage/TwoFactorAuthentication's view endpoint (AccountSecurityEndpoint or wherever the page is loaded). Compute via await userManager.CountRecoveryCodesAsync(user).
  • Update GenerateRecoveryCodesEndpoint (or its view counterpart) to pass the freshly generated codes through to the page once, and include a printable / downloadable rendering. Existing ShowRecoveryCodes page is the right place.
  • Front-end: extend the ShowRecoveryCodes page with Download and Print buttons. Print view = a dedicated <div> styled with a @media print rule that hides everything else.
  • Front-end: extend the 2FA management page with the remaining-count display + warning when low.
  • Do not add an endpoint that returns existing codes — by design, they are hashed and unretrievable. Add a comment in the endpoint explaining this so future contributors don't try.
  • Tests: recoveryCodesRemaining decreases after a redemption; warning threshold renders correctly at boundaries.

Benefits

  • Eliminates a self-inflicted lockout class for 2FA users.
  • Tiny implementation cost — CountRecoveryCodesAsync exists and works; the rest is markup.
  • Brings UX in line with industry norms — feature parity with GitHub / Microsoft / Google account pages.
  • Reduces support workload (recovery requests are expensive to handle securely).

Acceptance criteria

  • 2FA management page shows "X of N recovery codes remaining" using CountRecoveryCodesAsync.
  • Visible warning when ≤3 codes remain, with a path to regenerate.
  • After generation, the codes page offers a .txt download and a print-styled view.
  • No code path returns existing (hashed) codes — only newly generated ones at the moment of generation.
  • Tests for count display, threshold warning, and decrement after a redemption.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions