3.4 KiB
Console TOTP Recovery Codes Specification
Purpose
One-time recovery codes allow a user to authenticate when their authenticator app is unavailable. Covers generation, storage, redemption, and regeneration.
Requirements
Requirement: Recovery Code Generation at Enrollment
At successful enrollment (setup-verify), the system MUST generate exactly 8 recovery codes. Each code MUST be base32 alphanumeric, 10 characters long. Each code MUST be hashed with Argon2id before storage. Codes MUST be returned to the client in plaintext exactly once, in the setup-verify response, and MUST NOT be stored in plaintext anywhere.
Scenario: Enrollment produces exactly 8 codes
- GIVEN a user completes setup-verify with a valid code
- WHEN the enrollment transaction commits
- THEN exactly 8 rows exist in
two_factor_recovery_codesfor that user - AND each row contains an Argon2id hash, not the raw code
Scenario: Recovery codes returned once in plaintext
- GIVEN a user completes setup-verify
- WHEN the HTTP 200 response is received
- THEN
recovery_codesin the body contains exactly 8 base32 10-char strings - AND no subsequent endpoint exposes these codes again
Requirement: Recovery Code Redemption at Login
During login, recovery_code in the request body MUST be checked against stored hashes using Argon2id comparison. On a match, the matched row MUST be deleted (one-time use). No partial-use is permitted.
Scenario: Valid recovery code consumed and deleted
- GIVEN a user has 8 unused recovery codes
- WHEN
POST /api/auth/loginis submitted with one of those codes - THEN authentication succeeds (HTTP 200)
- AND that code's row is removed from
two_factor_recovery_codes - AND 7 rows remain
Scenario: Already-consumed recovery code rejected
- GIVEN a recovery code has been used (its row deleted)
- WHEN
POST /api/auth/loginis submitted with the same code - THEN the response is HTTP 401 with error code
RecoveryCodeInvalid
Scenario: Unknown recovery code rejected
- GIVEN no row in
two_factor_recovery_codesmatches the submitted code - WHEN
POST /api/auth/loginis submitted with that code - THEN the response is HTTP 401 with error code
RecoveryCodeInvalid
Requirement: Recovery Code Regeneration
The system MUST provide POST /api/auth/2fa/regenerate-recovery-codes for authenticated users with two_factor_enabled = true.
The request MUST include a currently valid TOTP code. On success, all existing recovery code rows for the user MUST be deleted and 8 new codes generated (same generation rules as enrollment). New codes MUST be returned in plaintext once in the response.
When the TOTP code is invalid, the endpoint MUST return HTTP 401 and MUST NOT alter existing codes.
Scenario: Valid TOTP → old codes deleted, 8 new codes returned
- GIVEN an authenticated user with
two_factor_enabled = trueand existing recovery code rows - WHEN
POST /api/auth/2fa/regenerate-recovery-codesis called with a valid TOTP code - THEN the response is HTTP 200 with
{ recovery_codes: [8 strings] } - AND all previous recovery code rows for that user are deleted
- AND 8 new hashed rows are inserted
Scenario: Invalid TOTP → 401, codes unchanged
- GIVEN an authenticated user with
two_factor_enabled = trueand existing recovery code rows - WHEN
POST /api/auth/2fa/regenerate-recovery-codesis called with a wrong TOTP code - THEN the response is HTTP 401
- AND existing recovery code rows are unchanged