Repository navigation
Description
Summary: Recovery codes are minted uppercase (crates/auth/src/mfa.rs::one_recovery_code) and verified via bcrypt::verify(code, hash) (crates/auth/src/mfa.rs:194-195) over the raw presented string — no case-normalization before hashing or verifying. A user who types their recovery code in lowercase (plausible if they wrote it down and re-typed it, rather than copy-pasting) gets a generic "invalid code" rejection with no indication that case is the problem.
Severity: S3 — user-facing degraded flow with a non-obvious workaround (retype in the exact stored case). The impact is compounded by the redemption route (crates/api/src/routes/mfa.rs:492-506) sharing its failure counter with the MFA challenge endpoint (record_mfa_failure_exhausted, referenced at crates/api/src/routes/mfa.rs:503-506, see also mfa_failure_counter_is_shared_across_challenge_and_recovery at crates/api/tests/test_mfa.rs:542) — a user retrying a case-mismatched code multiple times can lock themselves out of MFA verification entirely, during account recovery, without ever being told why their otherwise-correct code didn't work.
Environment:
- Deploy: any
- Commit: 6411ddb
- User / role: any user completing MFA recovery with a valid but wrong-case code
Repro steps:
- Enroll in MFA and obtain a recovery code (minted uppercase, format
XXXXX-XXXXX). - At the MFA challenge, submit the same code lowercased.
- Observe the redemption is rejected.
- Repeat several times (plausible if the user doesn't realize case matters) and observe the shared MFA-failure counter can rate-limit the account.
Expected: Either normalize case before verification (mint and compare case-insensitively), or reject with an error message that hints case sensitivity.
Observed: verify_recovery_code (crates/auth/src/mfa.rs:194-195) does a raw bcrypt compare; a lowercase-typed code fails with the same generic error as a wrong code, and repeated attempts count toward record_mfa_failure_exhausted's shared lockout threshold.
Evidence:
- Code:
crates/auth/src/mfa.rs:168-195(generate_recovery_codes/one_recovery_codemint uppercase;verify_recovery_codedoes a rawbcrypt::verify) - Code:
crates/api/src/routes/mfa.rs:492-506(redemption route, generic failure path, shared lockout counter) - Existing test:
recovery_code_bcrypt_roundtrip(crates/auth/src/mfa.rs:300-304) only asserts the exact-case roundtrip and an unrelated wrong-code rejection; it does not cover a same-code-different-case input.
Suspected area: crates/auth/src/mfa.rs::verify_recovery_code (normalize case before compare — the simplest fix, since codes are alphanumeric and minted uppercase already) and/or crates/api/src/routes/mfa.rs (redemption error message).
Activity