4.4 KiB
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-startis 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-startis 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-startis 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-startis 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-verifyis called with a currently-valid 6-digit code - THEN the response is HTTP 200 with
{ recovery_codes: [8 strings] } - AND
users.two_factor_enabledis set totrue - AND
users.two_factor_secret_encryptedholds 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-verifyis called with a wrong 6-digit code - THEN the response is HTTP 401 with error code
TwoFactorFailed - AND
users.two_factor_enabledremainsfalse - 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/disableis called with correct password and valid TOTP code - THEN the response is HTTP 200
- AND
users.two_factor_enabledisfalse - AND
users.two_factor_secret_encryptedis NULL - AND all rows in
two_factor_recovery_codesfor 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/disableis called with correct password but wrong TOTP code - THEN the response is HTTP 401
- AND
users.two_factor_enabledremainstrue
Scenario: Invalid password → 401, no change
- GIVEN an authenticated user with
two_factor_enabled = true - WHEN
POST /api/auth/2fa/disableis called with an incorrect password - THEN the response is HTTP 401
- AND
users.two_factor_enabledremainstrue