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

101 lines
4.4 KiB
Markdown

# Console TOTP Specification
## Purpose
TOTP enrollment, verification, and disable flows for platform console users. Covers the three-endpoint lifecycle: setup-start, setup-verify, and disable.
## Requirements
### Requirement: TOTP Enrollment Start
The system MUST provide `POST /api/auth/2fa/setup-start` accessible only to authenticated users holding a `platform_*` role.
When the user has no active enrollment (`two_factor_enabled = false`), the endpoint MUST return a freshly generated TOTP secret, its QR code as a base64-encoded PNG, and the `otpauth://` provisioning URI.
When the user already has `two_factor_enabled = true`, the endpoint MUST return HTTP 409.
#### Scenario: No prior enrollment → returns enrollment data
- GIVEN an authenticated user with `two_factor_enabled = false`
- WHEN `POST /api/auth/2fa/setup-start` is called
- THEN the response is HTTP 200 with `{ secret_base32, qr_code_png_base64, otpauth_uri }`
- AND no DB write is made at this stage (secret is pending verification)
#### Scenario: Already enrolled → 409
- GIVEN an authenticated user with `two_factor_enabled = true`
- WHEN `POST /api/auth/2fa/setup-start` is called
- THEN the response is HTTP 409 with error code `TwoFactorAlreadyEnabled`
#### Scenario: Unauthenticated caller → 401
- GIVEN a request with no valid session token
- WHEN `POST /api/auth/2fa/setup-start` is called
- THEN the response is HTTP 401
#### Scenario: Non-platform role → 403
- GIVEN an authenticated user without a `platform_*` role
- WHEN `POST /api/auth/2fa/setup-start` is called
- THEN the response is HTTP 403
---
### Requirement: TOTP Enrollment Verify
The system MUST provide `POST /api/auth/2fa/setup-verify` for confirming a pending enrollment.
When the submitted 6-digit code is valid against the in-progress secret, the endpoint MUST set `two_factor_enabled = true`, encrypt and store the secret, and return exactly 8 one-time recovery codes (plaintext, shown once).
When the code is invalid, the endpoint MUST return HTTP 401 and MUST NOT write anything to the database.
#### Scenario: Valid code → enrollment complete, recovery codes returned
- GIVEN a user who completed setup-start and has a pending secret
- WHEN `POST /api/auth/2fa/setup-verify` is called with a currently-valid 6-digit code
- THEN the response is HTTP 200 with `{ recovery_codes: [8 strings] }`
- AND `users.two_factor_enabled` is set to `true`
- AND `users.two_factor_secret_encrypted` holds the AES-256-GCM ciphertext of the secret
- AND 8 hashed recovery code rows are inserted into `two_factor_recovery_codes`
#### Scenario: Invalid code → 401, no DB writes
- GIVEN a user who completed setup-start
- WHEN `POST /api/auth/2fa/setup-verify` is called with a wrong 6-digit code
- THEN the response is HTTP 401 with error code `TwoFactorFailed`
- AND `users.two_factor_enabled` remains `false`
- AND no rows are written to `two_factor_recovery_codes`
---
### Requirement: TOTP Disable
The system MUST provide `POST /api/auth/2fa/disable` for an authenticated user to remove their TOTP enrollment.
The request MUST supply both the current password and a current valid TOTP code. Both MUST be verified server-side. If either fails, the endpoint returns HTTP 401 and makes no DB changes.
On success, the system MUST clear `two_factor_enabled`, `two_factor_secret_encrypted`, `two_factor_secret_nonce`, `last_totp_step_used`, `totp_locked_until`, and all rows in `two_factor_recovery_codes` for that user.
#### Scenario: Valid password + valid TOTP → disabled
- GIVEN an authenticated user with `two_factor_enabled = true`
- WHEN `POST /api/auth/2fa/disable` is called with correct password and valid TOTP code
- THEN the response is HTTP 200
- AND `users.two_factor_enabled` is `false`
- AND `users.two_factor_secret_encrypted` is NULL
- AND all rows in `two_factor_recovery_codes` for that user are deleted
#### Scenario: Valid password, invalid TOTP → 401, no change
- GIVEN an authenticated user with `two_factor_enabled = true`
- WHEN `POST /api/auth/2fa/disable` is called with correct password but wrong TOTP code
- THEN the response is HTTP 401
- AND `users.two_factor_enabled` remains `true`
#### Scenario: Invalid password → 401, no change
- GIVEN an authenticated user with `two_factor_enabled = true`
- WHEN `POST /api/auth/2fa/disable` is called with an incorrect password
- THEN the response is HTTP 401
- AND `users.two_factor_enabled` remains `true`