Files
OmniOilPersonal/openspec/changes/console-totp-2fa/specs/console-totp-recovery/spec.md
2026-05-08 05:37:24 -05:00

77 lines
3.4 KiB
Markdown

# 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_codes` for 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_codes` in 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/login` is 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/login` is 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_codes` matches the submitted code
- WHEN `POST /api/auth/login` is 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 = true` and existing recovery code rows
- WHEN `POST /api/auth/2fa/regenerate-recovery-codes` is 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 = true` and existing recovery code rows
- WHEN `POST /api/auth/2fa/regenerate-recovery-codes` is called with a wrong TOTP code
- THEN the response is HTTP 401
- AND existing recovery code rows are unchanged