145 lines
6.2 KiB
Markdown
145 lines
6.2 KiB
Markdown
# Delta for Auth — console-totp-2fa
|
|
|
|
## MODIFIED Requirements
|
|
|
|
### Requirement: Login Response Shape
|
|
|
|
The login endpoint MUST return a structured 401 with `requires_2fa: true` when `two_factor_enabled = true` and no TOTP or recovery code was supplied. It MUST NOT return HTTP 500 for this case.
|
|
|
|
When `two_factor_enabled = true` and a valid `totp_code` or `recovery_code` is supplied, the endpoint MUST return 200 with `access_token` and set the refresh cookie.
|
|
|
|
When `two_factor_enabled = false`, existing login behavior MUST remain unchanged.
|
|
|
|
(Previously: absent code returned HTTP 500 with an unstructured Spanish error message; no `requires_2fa` field existed.)
|
|
|
|
#### Scenario: 2FA enabled — code absent → structured 401
|
|
|
|
- GIVEN a user with `two_factor_enabled = true`
|
|
- WHEN `POST /api/auth/login` is called with valid email + password but no `totp_code` or `recovery_code`
|
|
- THEN the response is HTTP 401
|
|
- AND the body contains `{ "error": "...", "requires_2fa": true }`
|
|
|
|
#### Scenario: 2FA enabled — valid TOTP code → 200
|
|
|
|
- GIVEN a user with `two_factor_enabled = true`
|
|
- WHEN `POST /api/auth/login` is called with valid credentials and a valid 6-digit `totp_code`
|
|
- THEN the response is HTTP 200 with `access_token`
|
|
- AND the refresh cookie is set
|
|
|
|
#### Scenario: 2FA enabled — invalid TOTP code → 401 TwoFactorFailed
|
|
|
|
- GIVEN a user with `two_factor_enabled = true`
|
|
- WHEN `POST /api/auth/login` is called with valid credentials but a wrong `totp_code`
|
|
- THEN the response is HTTP 401 with error code `TwoFactorFailed`
|
|
|
|
#### Scenario: 2FA enabled — valid recovery code → 200, code consumed
|
|
|
|
- GIVEN a user with `two_factor_enabled = true` and at least one unused recovery code
|
|
- WHEN `POST /api/auth/login` is called with `recovery_code` matching a stored hash
|
|
- THEN the response is HTTP 200 with `access_token`
|
|
- AND that recovery code row is DELETED from `two_factor_recovery_codes`
|
|
|
|
#### Scenario: 2FA enabled — already-used recovery code → 401
|
|
|
|
- GIVEN a user with `two_factor_enabled = true`
|
|
- WHEN `POST /api/auth/login` is called with a `recovery_code` that has already been consumed
|
|
- THEN the response is HTTP 401 with error code `RecoveryCodeInvalid`
|
|
|
|
#### Scenario: 2FA disabled — login unchanged
|
|
|
|
- GIVEN a user with `two_factor_enabled = false`
|
|
- WHEN `POST /api/auth/login` is called with valid email + password
|
|
- THEN the response is HTTP 200 with `access_token` (existing behavior preserved)
|
|
|
|
---
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: Lockout After Repeated TOTP Failures
|
|
|
|
The system MUST track failed TOTP attempts per user. After 5 consecutive failures within 15 minutes, `users.totp_locked_until` MUST be set to `now() + 30 minutes`.
|
|
|
|
While `users.totp_locked_until > now()`, all login attempts for that user MUST return HTTP 401 with error code `AccountLocked` — regardless of whether the password is correct (to avoid leaking password validity during lockout).
|
|
|
|
On a successful TOTP verification, the failure counter MUST be reset.
|
|
|
|
#### Scenario: Lockout threshold reached → account locked
|
|
|
|
- GIVEN a user has submitted 5 wrong TOTP codes within 15 minutes
|
|
- WHEN the 5th failure is processed
|
|
- THEN `users.totp_locked_until` is set to approximately `now() + 30 minutes`
|
|
|
|
#### Scenario: Login while locked → 401 AccountLocked (correct password)
|
|
|
|
- GIVEN `users.totp_locked_until > now()` for a user
|
|
- WHEN `POST /api/auth/login` is called with the correct password
|
|
- THEN the response is HTTP 401 with error code `AccountLocked`
|
|
- AND no information about password correctness is revealed
|
|
|
|
#### Scenario: Login while locked → 401 AccountLocked (wrong password)
|
|
|
|
- GIVEN `users.totp_locked_until > now()` for a user
|
|
- WHEN `POST /api/auth/login` is called with an incorrect password
|
|
- THEN the response is HTTP 401 with error code `AccountLocked`
|
|
|
|
#### Scenario: Successful TOTP resets failure counter
|
|
|
|
- GIVEN a user has 3 previous TOTP failures within 15 minutes
|
|
- WHEN `POST /api/auth/login` succeeds with a valid TOTP code
|
|
- THEN the TOTP failure counter for that user is reset to 0
|
|
|
|
---
|
|
|
|
### Requirement: TOTP Replay Protection
|
|
|
|
The system MUST record `users.last_totp_step_used` on every successful TOTP verification.
|
|
|
|
A TOTP code MUST only be accepted if the step it validates is strictly greater than `last_totp_step_used` (or `last_totp_step_used` is NULL), within the ±1 skew window.
|
|
|
|
#### Scenario: First successful TOTP stores step
|
|
|
|
- GIVEN `users.last_totp_step_used` is NULL
|
|
- WHEN a valid TOTP code is accepted
|
|
- THEN `users.last_totp_step_used` is set to the current step value
|
|
|
|
#### Scenario: Re-use of same step within 30s window → 401
|
|
|
|
- GIVEN `users.last_totp_step_used = S` for some user
|
|
- WHEN `POST /api/auth/login` is submitted with a code that maps to step S (or S-1)
|
|
- THEN the response is HTTP 401 with error code `TwoFactorFailed`
|
|
- AND `last_totp_step_used` is NOT updated
|
|
|
|
#### Scenario: New step (S+1 or later) accepted
|
|
|
|
- GIVEN `users.last_totp_step_used = S`
|
|
- WHEN a code mapping to step S+1 (or later) within ±1 skew is submitted
|
|
- THEN the code is accepted and `last_totp_step_used` is updated to the new step
|
|
|
|
---
|
|
|
|
### Requirement: TOTP Secret Encryption Invariant
|
|
|
|
`users.two_factor_secret_encrypted` MUST NEVER contain plaintext. The secret MUST be encrypted with AES-256-GCM using `TOTP_SECRET_ENCRYPTION_KEY` before writing to the database.
|
|
|
|
The application MUST read `TOTP_SECRET_ENCRYPTION_KEY` from the environment at startup. If the key is absent or invalid, the application MUST refuse to start.
|
|
|
|
Decrypted secrets MUST be held in-memory only for the duration of the operation and MUST NOT be logged or persisted.
|
|
|
|
#### Scenario: Startup without encryption key → boot failure
|
|
|
|
- GIVEN `TOTP_SECRET_ENCRYPTION_KEY` is not set in the environment
|
|
- WHEN the backend application starts
|
|
- THEN the process exits with a non-zero status code before accepting connections
|
|
|
|
#### Scenario: Startup with valid key → normal boot
|
|
|
|
- GIVEN `TOTP_SECRET_ENCRYPTION_KEY` is a valid 32-byte base64 string
|
|
- WHEN the backend application starts
|
|
- THEN the process boots normally
|
|
|
|
#### Scenario: DB dump never exposes plaintext
|
|
|
|
- GIVEN a user has `two_factor_enabled = true`
|
|
- WHEN the `users` table is dumped from the database
|
|
- THEN `two_factor_secret_encrypted` contains ciphertext (BYTEA), never a readable TOTP seed
|