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

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