sec(auth): unify login error response for unknown vs wrong-password (closes #416)#887
Conversation
|
@coderabbitai review |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThe PR hardens login authentication against enumeration attacks by centralizing all credential-validation failures into a single generic error message and equalizing bcrypt timing across success and failure paths. A precomputed bcrypt hash is introduced, and the Login service now invokes password verification even for missing-user scenarios to eliminate response-time side-channels. ChangesUnified Authentication Error Messages and Timing Equalization
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
✅ Actions performedReview triggered.
|
…loses #416) Collapse two distinct login failure messages into a single generic response "invalid email or password" for all authentication failure paths: - GetUserByEmail returns error (real store path for missing row): was "authentication failed", now "invalid email or password" - user == nil, inactive user, locked user, empty password hash, wrong password: all now return "invalid email or password" Add a dummy bcrypt.CompareHashAndPassword call in Login when getUserAndValidateStatus fails so the timing of a missing-account request matches a wrong-password request on a real account, closing the timing-side-channel oracle alongside the message oracle. Coordination with #388: #388 fixes MFA-enrollment leakage at the handler layer (service errors mapped to machine-readable codes). That PR has no open worktree/PR and addresses a different path (post-password-check MFA signals). This PR fixes the pre-password-check path. The two changes are non-overlapping; whichever lands first, the other rebases cleanly. Tests: extend TestLogin_OWASPEnumerationInvariant from 5 to 6 scenarios by adding the real Postgres store error path ((nil, err) not (nil, nil)); add TestHandler_login_ErrorEquivalence which positively asserts that both paths produce identical HTTP status and body at the handler layer.
|
Rebased Note: CodeRabbit has not yet produced a review pass on this PR; the only prior CR comment was a rate-limit / org-usage-credit notice. Triggering a fresh pass now. @coderabbitai review |
|
🧠 Learnings used✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
Summary
"invalid email or password"for all authentication failure paths, eliminating the account-enumeration oracle reported in sec(auth): login returns distinct error messages for non-existent vs existing accounts (account enumeration) #416bcrypt.CompareHashAndPasswordcall inLoginwhen the user-lookup fails so a missing-account request takes the same wall time as a wrong-password request on a real account, closing the timing-side-channel oracleTestLogin_OWASPEnumerationInvarianttest from 5 to 6 scenarios by adding the real Postgres store-error path ((nil, err)not(nil, nil)) and addsTestHandler_login_ErrorEquivalenceat the handler layerCoordination with #388
PR #886 (closes #388) fixes MFA-enrollment leakage at the handler layer (service errors mapped to machine-readable codes after the password check passes). This PR fixes the pre-password-check path (distinct messages for missing vs existing users). The two changes are non-overlapping in terms of code paths and diff regions; whichever lands first, the other rebases cleanly. If both go out in the same merge window, rebase this one on top of #886.
Files changed
internal/auth/service.go-- unified error messages ingetUserAndValidateStatusandverifyPasswordAndMFA; dummy bcrypt call inLogininternal/auth/service_password.go-- addeddummyPasswordHashsentinel var (pre-computed bcrypt-12 hash)internal/auth/service_test.go-- updated 5 existing assertionsinternal/auth/service_lockout_test.go-- updated 2 assertions, added store-error scenario to OWASP invariant testinternal/api/handler_auth_test.go-- addedTestHandler_login_ErrorEquivalenceTest plan
go test ./internal/auth/... ./internal/api/...)TestLogin_OWASPEnumerationInvariantnow has 6 scenarios and covers both(nil, nil)and(nil, err)store return shapesTestHandler_login_ErrorEquivalencepositively asserts identical HTTP status (401) and body (invalid email or password) for both the unknown-user and wrong-password paths"authentication failed"or"Check your email address and password"strings remain in any login-flow error pathSummary by CodeRabbit