Skip to content

feat: recovery codes and restricted recovery sessions - #491

Merged
Bccorb merged 5 commits into
mainfrom
feat/recovery-codes
Oct 23, 2026
Merged

Bccorb merged 5 commits into
mainfrom
feat/recovery-codes

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Closes #398. Depends on #482 (types 0.30.0) and #489 (auth secrets); this branch merges both, so the diff shrinks once they land.

Recovery codes

  • GET /recovery-codes (access): { enabled, remaining, total, createdAt }.
  • POST /recovery-codes (access; step-up when a set exists): ten single-use base32 codes XXXXX-XXXXX, shown once, replacing the old set.
  • POST /recovery-codes/redeem (pre-auth, decoy-safe, OTP flow limits): spends a code with a conditional update and issues a recovery session.
  • Stored as HMAC-SHA256 keyed with REFRESH_TOKEN_LOOKUP_SECRET (domain-separated); rotating that secret voids codes, documented.
  • Not subject to lockout and not counted towards it (same reasoning as passkeys after GHSA-66gq). Not a login method; gated by recovery_codes.enabled only. Allowed in phishing-resistant-only mode.

Recovery sessions (contract: fells-code/seamless-auth-types#108)

  • sessions.recovery on the row (refresh keeps it), recovery: true claim in the access token, recovery: true + nextStep: 'enroll_passkey' in the response.
  • verifyBearerAuth answers 403 enrollment_required unless the route sets the new defineRoute option allowRecoverySession: passkey and TOTP enrollment, TOTP status, recovery codes, GET /users/me, /logout, /logout/all.
  • Passkey registration and TOTP enrollment from a recovery session lift the restriction on the row (liftRecoveryRestriction); clients should refresh to drop the claim from the token.
  • GET /system-config/public adds recoveryCodes.

Left for follow-ups

Checks

typecheck, lint, prettier, build, coverage (144 files, 2010 tests; statements 98.8%, branches 95.26%). Migration 20261022140000-add-recovery-codes run up, down, up on a scratch Postgres 18. generate:api output committed (adapter manifest gains POST /recovery-codes/redeem preAuth -> session).

Contract-affecting: new routes, new 403 enrollment_required on access routes for recovery sessions, new token claim. Adapters pick up the redeem route from the manifest; the access routes pass through by default.

A user can generate single-use codes and sign in with one; the session it issues can only
enroll a new factor until one is enrolled.
A locked account with no passkey, or one the policy leaves with no method, continues with a
pre-auth token when it holds an unused recovery code.
# Conflicts:
#	docs/security-posture.md
#	resources/coverage-badge.svg
#	src/controllers/authentication.ts
#	src/services/sessionIssuance.ts
@Bccorb
Bccorb merged commit 615b853 into main Oct 23, 2026
9 checks passed
@Bccorb
Bccorb deleted the feat/recovery-codes branch October 23, 2026 01:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: recovery codes

1 participant