# 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`