Skip to content

sec(auth): unify login error response for unknown vs wrong-password (closes #416)#887

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/416-wave21
Jun 3, 2026
Merged

sec(auth): unify login error response for unknown vs wrong-password (closes #416)#887
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/416-wave21

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 30, 2026

Summary

  • Collapses two distinct login failure messages into the single generic response "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) #416
  • Adds a dummy bcrypt.CompareHashAndPassword call in Login when 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 oracle
  • Extends the existing TestLogin_OWASPEnumerationInvariant test from 5 to 6 scenarios by adding the real Postgres store-error path ((nil, err) not (nil, nil)) and adds TestHandler_login_ErrorEquivalence at the handler layer

Coordination 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 in getUserAndValidateStatus and verifyPasswordAndMFA; dummy bcrypt call in Login
  • internal/auth/service_password.go -- added dummyPasswordHash sentinel var (pre-computed bcrypt-12 hash)
  • internal/auth/service_test.go -- updated 5 existing assertions
  • internal/auth/service_lockout_test.go -- updated 2 assertions, added store-error scenario to OWASP invariant test
  • internal/api/handler_auth_test.go -- added TestHandler_login_ErrorEquivalence

Test plan

  • All 1854 existing tests pass (go test ./internal/auth/... ./internal/api/...)
  • TestLogin_OWASPEnumerationInvariant now has 6 scenarios and covers both (nil, nil) and (nil, err) store return shapes
  • TestHandler_login_ErrorEquivalence positively asserts identical HTTP status (401) and body (invalid email or password) for both the unknown-user and wrong-password paths
  • No "authentication failed" or "Check your email address and password" strings remain in any login-flow error path

Summary by CodeRabbit

  • Bug Fixes
    • Standardized authentication error messages: login failures now consistently indicate "invalid email or password" regardless of the specific cause.
    • Improved timing consistency in password verification operations.

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy urgency/this-quarter Within the quarter impact/many Affects most users effort/m Days type/security Security finding labels May 30, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6515e34-b1f5-443f-bdaf-999dcb51dad2

📥 Commits

Reviewing files that changed from the base of the PR and between 56a7797 and e859a90.

📒 Files selected for processing (5)
  • internal/api/handler_auth_test.go
  • internal/auth/service.go
  • internal/auth/service_lockout_test.go
  • internal/auth/service_password.go
  • internal/auth/service_test.go

📝 Walkthrough

Walkthrough

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

Changes

Unified Authentication Error Messages and Timing Equalization

Layer / File(s) Summary
Timing constant and Login service unification
internal/auth/service_password.go, internal/auth/service.go
dummyPasswordHash bcrypt constant is added; Login now runs dummy password verification when user validation fails to equalize timing; getUserAndValidateStatus and verifyPasswordAndMFA unify all failure cases into "invalid email or password" regardless of whether the user exists, is inactive, is locked, or submitted wrong credentials.
Service-layer test updates
internal/auth/service_test.go, internal/auth/service_lockout_test.go
All login failure assertions are updated to expect the unified "invalid email or password" message; OWASP enumeration-invariant test adds a new storeError field to verify that store errors and nil-user scenarios both produce identical responses.
Handler-level regression test
internal/api/handler_auth_test.go
New regression test TestHandler_login_ErrorEquivalence mocks two distinct failure scenarios (non-existent user and wrong password), confirms both return ClientError, and asserts both produce identical status code and message body.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#551: Both PRs modify internal/auth/service.go and associated login/lockout tests to unify error messages across multiple failed authentication paths.

Suggested labels

severity/high, effort/s

Poem

🐰 Hop, hop! No more whispers of who exists and who's locked away,
Same error for all fails—the attacker learns nothing today.
Dummy bcrypt runs the race, timing equalized with grace,
Account enumeration bugs? Hopped right out of this place! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: unifying login error responses for unknown vs wrong-password cases to close issue #416.
Linked Issues check ✅ Passed The PR addresses issue #388 by unifying error messages and adding dummy bcrypt timing mitigation; tests verify identical 401 responses across failure scenarios.
Out of Scope Changes check ✅ Passed All changes directly support the core objectives: unified error messages, timing mitigation, and comprehensive test coverage for enumeration prevention.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/416-wave21

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…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.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented Jun 3, 2026

Rebased fix/416-wave21 onto the latest feat/multicloud-web-frontend. Resolved one additive conflict in internal/api/handler_auth_test.go: the base added the getCurrentUserPermissions tests (issue #917/#922) and this PR added TestHandler_login_ErrorEquivalence (issue #416) in the same trailing region of the file. Kept both test suites; the two changes are independent. Verified post-rebase: gofmt clean, go vet clean, build OK, and go test ./internal/auth/... ./internal/api/... all green (1905 tests, including both merged suites).

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

@cristim Acknowledged — triggering a fresh review pass on the rebased branch. The additive merge of TestHandler_login_ErrorEquivalence alongside the getCurrentUserPermissions tests sounds clean; I'll cover both suites in the pass.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented Jun 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy triaged Item has been triaged type/security Security finding urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant