ux(auth): soften login error copy (closes #550) - #551
Conversation
Replace "invalid email or password" with "Check your email address and password and try again" across all 5 authentication failure paths in internal/auth/service.go (user-not-found, inactive, locked, no-password- hash, wrong-password). The OWASP ASVS V3.3.4 / V14.2 invariant is preserved: all five paths continue to return an identical string so the endpoint cannot be used as an email-existence oracle. The new TestLogin_OWASPEnumerationInvariant table-driven test in service_lockout_test.go guards this invariant going forward -- a future PR that accidentally re-introduces a path-specific message will fail that test. Frontend: mapServerLoginError in auth.ts updated to match and surface the new copy. Tests in auth.test.ts and api-mfa.test.ts updated to assert on the new string.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughBackend and frontend login failures now return the single message "Check your email address and password and try again" for all credential validation failures; tests updated and a new table-driven OWASP enumeration-invariant test ensures all failure paths produce the identical message. ChangesUnified authentication error messaging
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
frontend/src/auth.ts (1)
653-657: ⚡ Quick winKeep legacy backend error mapping for rollout compatibility.
If any older node still returns
invalid email or password, users will see inconsistent copy. Map it to the same unified message.♻️ Suggested tweak
if ( lower.includes('authentication failed') || + lower.includes('invalid email or password') || lower.includes('check your email address and password') ) { return 'Check your email address and password and try again'; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/auth.ts` around lines 653 - 657, The current error-mapping block checks for "authentication failed" and "check your email address and password" on the lowercase error text (variable lower) and returns the unified message; add an additional branch to treat the legacy backend string "invalid email or password" the same way by checking lower.includes('invalid email or password') and returning the identical unified message so older nodes produce the same copy; update the same conditional (the if that references lower) to include this legacy phrase.internal/auth/service.go (1)
143-190: ⚡ Quick winCentralize the generic login-failure copy into one constant.
This string is duplicated across multiple auth-failure branches. A future single-branch edit can accidentally break consistency.
♻️ Suggested refactor
const ( + GenericLoginFailureMessage = "Check your email address and password and try again" // PasswordResetExpiry is how long password reset tokens are valid PasswordResetExpiry = 1 * time.Hour @@ if user == nil { - return nil, fmt.Errorf("Check your email address and password and try again") + return nil, fmt.Errorf(GenericLoginFailureMessage) } @@ if !user.Active { - return nil, fmt.Errorf("Check your email address and password and try again") + return nil, fmt.Errorf(GenericLoginFailureMessage) } @@ - return nil, fmt.Errorf("Check your email address and password and try again") + return nil, fmt.Errorf(GenericLoginFailureMessage) } @@ if user.PasswordHash == "" { - return fmt.Errorf("Check your email address and password and try again") + return fmt.Errorf(GenericLoginFailureMessage) } @@ - return fmt.Errorf("Check your email address and password and try again") + return fmt.Errorf(GenericLoginFailureMessage) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/auth/service.go` around lines 143 - 190, Centralize the repeated generic login-failure string by adding a package-level constant (e.g. GenericLoginFailureMsg = "Check your email address and password and try again") and replace all literal occurrences that return fmt.Errorf(...) with fmt.Errorf(GenericLoginFailureMsg); update uses in verifyPasswordAndMFA (the PasswordHash and failed password branches) and the earlier auth checks that return the same message so every auth-failure branch (including the account-inactive and locked-account returns) uses the single named constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/auth/service_lockout_test.go`:
- Around line 418-437: Each subtest in the scenarios loop sets expectations on
the mocks (MockStore, MockEmailSender) but never asserts them; add assertion(s)
inside the t.Run closure after invoking service.Login to validate the expected
mock interactions. Specifically, call mockStore.AssertExpectations(t) and
mockEmail.AssertExpectations(t) (or a combined AssertExpectations on each mock)
immediately after the require/assert checks in the anonymous test function that
uses createTestService and service.Login so failures in expected calls are
reported per-subtest.
---
Nitpick comments:
In `@frontend/src/auth.ts`:
- Around line 653-657: The current error-mapping block checks for
"authentication failed" and "check your email address and password" on the
lowercase error text (variable lower) and returns the unified message; add an
additional branch to treat the legacy backend string "invalid email or password"
the same way by checking lower.includes('invalid email or password') and
returning the identical unified message so older nodes produce the same copy;
update the same conditional (the if that references lower) to include this
legacy phrase.
In `@internal/auth/service.go`:
- Around line 143-190: Centralize the repeated generic login-failure string by
adding a package-level constant (e.g. GenericLoginFailureMsg = "Check your email
address and password and try again") and replace all literal occurrences that
return fmt.Errorf(...) with fmt.Errorf(GenericLoginFailureMsg); update uses in
verifyPasswordAndMFA (the PasswordHash and failed password branches) and the
earlier auth checks that return the same message so every auth-failure branch
(including the account-inactive and locked-account returns) uses the single
named constant.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4f570af8-072b-4b30-aa85-dbd24f85e7f3
📒 Files selected for processing (6)
frontend/src/__tests__/api-mfa.test.tsfrontend/src/__tests__/auth.test.tsfrontend/src/auth.tsinternal/auth/service.gointernal/auth/service_lockout_test.gointernal/auth/service_test.go
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
The new TestLogin_OWASPEnumerationInvariant table-driven test had the two trailing mockStore/mockEmail.AssertExpectations calls indented one tab short of the t.Run closure, which gofmt rewrote on the pre-commit hook. Apply the rewrite ourselves so the hook is clean.
|
Fixed gofmt indentation drift on the OWASP invariant test (commit pushed). Pre-commit hooks should now pass on the new run. |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary
"invalid email or password"with the friendlier"Check your email address and password and try again"across all 5 authentication failure paths ininternal/auth/service.go(user-not-found, inactive, locked, empty-password-hash, wrong-password).mapServerLoginErrorinauth.tsupdated to match and surface the new copy; the function still collapses both"authentication failed"and the new backend string into the same display message, preserving the OWASP non-enumeration invariant (closes ux(login): wrong-data error messages are generic ("authentication failed") and hide which field is wrong #456 regression risk noted in ux(auth): soften 'Incorrect email or password' copy without leaking enumeration signal #550).OWASP rationale (closing note on #550 suggestions)
QA suggestions #1, #3, and #4 in issue #550 (distinct per-scenario messages) were explicitly rejected because they would turn the login endpoint into an email-existence oracle (OWASP ASVS V3.3.4 / V14.2). This PR ships only the copy softening: semantics unchanged, no info leak, friendlier tone.
OWASP invariant regression guard
Added
TestLogin_OWASPEnumerationInvariantininternal/auth/service_lockout_test.go-- a table-driven test that exercises all 5 failure modes and asserts each returns the same error string. Any future PR that accidentally re-introduces a path-specific message will fail this test.Test plan
go test ./internal/auth/... ./internal/api/...-- all pass (497 + 1151 tests)go vet ./...-- cleanauth,api-mfasuites) -- 76 tests passnpx tsc --noEmit-- no type errorsgit grep "invalid email or password" internal/ pkg/ frontend/src/-- no hitsSummary by CodeRabbit
Bug Fixes
Tests