Repository navigation
fix(auth): lock the account on wrong MFA codes at enable, disable and regenerate - #598
Conversation
… regenerate MFAEnable, MFADisable and MFARegenerateRecoveryCodes now call recordFailedLogin on a wrong or replayed code, sharing Login's counter, MaxFailedLoginAttempts and AccountLockoutDuration. A locked account is refused with the opaque ErrMFAAuthFailed before any code is checked. Empty codes are not counted. Refs #442
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (4)
Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. 📝 WalkthroughWalkthroughMFA enable, disable, and recovery-code regeneration now reject locked accounts. Non-empty invalid or replayed codes record failed logins; empty codes do not. Tests cover shared lockout thresholds, state preservation while locked, failure-count behavior, concurrent failures, and HTTP responses. ChangesMFA lifecycle lockout
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to No actionable merge-blocking issue was established; the change is ready for normal merge checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
Rename the lockout and claim errors so govet shadow stays clean, and make the locked-account unit test fail on the unfixed service for enable, disable and regenerate. Drop the empty-code unit test, covered by the integration suite.
… per account (#1000) The two items in the #442 body landed in #596 (TOTP counter) and #598 (lockout on lifecycle failures); this closes what was left open on the issue: - MFAEnable passes the enrollment code's TOTP counter to EnableMFA, which sets users.mfa_last_totp_counter in the same conditional UPDATE, so the code typed to turn MFA on cannot be replayed at Login inside its window. The counter is replaced, not raised, because it belonged to the previous secret. - Login's TOTP / recovery-code leg gets a per-account "login_mfa" bucket (10 / 15 min, keyed by the parsed, lower-cased email, fail-closed), so rotating source IPs no longer resets the code-guessing budget. - requireMFASession validates the session before the per-IP "mfa" bucket, so requests without a valid session do not spend that IP's budget. Closes #442 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Part 2 of #442 (part 1, TOTP replay, merged as #596). Wrong codes on the MFA lifecycle methods now feed the same durable lockout as Login.
Changes
MFAEnable,MFADisable(TOTP and recovery code) andMFARegenerateRecoveryCodescallrecordFailedLoginon a wrong code, so they share Login's counter,MaxFailedLoginAttemptsandAccountLockoutDuration. The count is the existing atomic SQL update.Decisions for review
ErrMFAAuthFailed(opaque 401), the existing sentinel. Login's equivalent is also an opaque 401, so no new API contract.RecordSuccessfulLogin, which also bumpslast_login_at), so the same holds here.mfa_requiredwithout counting).MFADisableis not counted (the issue is about codes).ValidateSessiondoes not look at the lock today, and revoking is a separate behavior change.MFASetupis unchanged (password only, no code).Not in this PR
rate_limiter.go,handler_auth.go): Login already counts MFA failures toward the durable lockout, so rotating IPs is bounded; a per-user rate-limit key is a separate change.mfabucket inrequireMFASession(middleware.go~578).Tests
service_mfa_lockout_db_test.go(integration, real Postgres): Login baseline locks at N; Disable, Regenerate and Enable each lock at N wrong codes and then refuse a correct code with no state change; correct code below the threshold succeeds and keeps the count; replay counts; empty code does not; N concurrent failures count exactly N.server/mfa_lockout_dispatch_test.go: throughHandleRequeston/api/auth/mfa/disable, N wrong codes lock, the next request is refused with 401 and touches neither TOTP claim nor disable.RecordFailedLogin.Refs #442
Summary by CodeRabbit
(c) The per-user key on Login's TOTP leg and the session-before-IP-bucket ordering stay open on #442.