Skip to content

fix(auth): normalize email case in login - #185

Merged
argszero merged 1 commit into
mainfrom
fix/login-email-normalize
Sep 12, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/login-email-normalize

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

POST /api/auth/login was the only email-taking handler that did not normalize its input: register / verify / resend-code / forgot-password / reset-password all do req.email.trim().to_lowercase(), and registration stores the lowercased form. Since users.email is TEXT NOT NULL UNIQUE with no COLLATE NOCASE, SQLite = is case-sensitive, so any case-varied input was treated as "user does not exist" → 401, even though the account exists and the password is correct.

This is reachable in practice: ui/index.html uses type="email" without autocapitalize="none", so a mobile keyboard auto-capitalizes the first letter and the user is locked out of their own account with a correct password — while forgot-password (which does normalize) still finds the account and returns "rate limited".

Related Issue

None — no open issue describes this. Verified closingIssuesReferences is empty on purpose; nothing is fabricated.

Changes

  • Normalize the email in login (src/routes/mod.rs) with the same trim().to_lowercase() the five sibling handlers already use, then pass it to dao::verify_user_password. The 401 message is deliberately unchanged and still does not distinguish "no such user" from "wrong password" (that would reintroduce an account-enumeration oracle this module otherwise avoids).
  • 涉及配置/数据结构的改动已同步示例文件 — not applicable (no config/schema change; explicitly not adding COLLATE NOCASE, which would need a migration and change UNIQUE semantics).

Tests

  • cargo test 全部通过 — 168 passed / 0 failed (baseline 167 + the new test).
  • cargo fmt --check 通过
  • 新增/更新了单元测试 — login_normalizes_email_case: registers with a mixed-case email (asserting it is stored as the lowercase canonical form), verifies it, then asserts both the canonical lowercase login and the mixed-case variant return 200. The canonical-lowercase assertion is a positive control so the test cannot pass by accepting anything.

A/B of the test itself — with only the handler change reverted, the new test fails exactly at the property assertion:

test routes::tests::login_normalizes_email_case ... FAILED
assertion `left == right` failed: 大小写变体登录应 200: {"error":"unauthorized"}
  left: 401
 right: 200

With the fix in place it passes. So the test genuinely pins the defect rather than the fix.

Checklist

  • 分支命名符合约定 — fix/login-email-normalize
  • Commit message 使用 Conventional Commits 格式 — fix(auth): normalize email case in login
  • 单一职责,改动最小化 — 1 file, +58/−1 (1 line of production code + comment, 54 lines of test)

`POST /api/auth/login` was the only email-taking handler that did not
normalize its input. `register`/`verify`/`resend-code`/`forgot-password`/
`reset-password` all do `req.email.trim().to_lowercase()`, and registration
stores the lowercased form. `users.email` has no `COLLATE NOCASE`, so SQLite
`=` is case-sensitive and any case-varied input (e.g. a mobile keyboard
auto-capitalizing the first letter) failed with 401 "unauthorized" even
though the account exists and the password is correct.

Add the same normalization to `login` before the lookup, and a regression
test that registers with a mixed-case email, verifies it, then asserts that
both the canonical lowercase and the mixed-case variant can log in.
@argszero
argszero merged commit 223ad55 into main Sep 12, 2026
1 check passed
@argszero
argszero deleted the fix/login-email-normalize branch September 12, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant