fix(auth): normalize user emails to lowercase on write #333

Merged
Claude-Run merged 1 commit from fix/BUNYIP-325-normalize-email-lowercase into main 2026-07-04 02:36:43 +02:00
Member

Signing up with a mixed-case email ("Nice.Guy@Example.COM") stored the address verbatim. Lookups already compared with LOWER(email) so login kept working, but the stored address (and the OIDC email claim and the address outbound verification / welcome mail is sent to) diverged in case from what case-sensitive downstream consumers expect, so the verification mail was never reconciled and the account stayed stuck unverified.

Funnel every write to users.email through a new normalize_email() helper (lowercase only, matching the untrimmed LOWER($1) read side) at the two and only write paths: UserRepository::create (the single INSERT choke point that every signup surface inherits) and UserRepository::update_email (the email-change path). Back the stored value with a one-shot backfill migration that lowercases pre-existing rows; it is collision-safe because the BUNYIP-330 users_email_unique index is already on LOWER(email), and idempotent via a WHERE email <> LOWER(email) guard.

Unit-test the helper (mixed-case lowercased, idempotent on already-lowercase). The other three INSERT INTO users sites are non-production e2e/test-only with lowercase-by-construction emails, so the invariant holds workspace-wide.

#BUNYIP-325

Signing up with a mixed-case email ("Nice.Guy@Example.COM") stored the address verbatim. Lookups already compared with LOWER(email) so login kept working, but the stored address (and the OIDC email claim and the address outbound verification / welcome mail is sent to) diverged in case from what case-sensitive downstream consumers expect, so the verification mail was never reconciled and the account stayed stuck unverified. Funnel every write to users.email through a new normalize_email() helper (lowercase only, matching the untrimmed LOWER($1) read side) at the two and only write paths: UserRepository::create (the single INSERT choke point that every signup surface inherits) and UserRepository::update_email (the email-change path). Back the stored value with a one-shot backfill migration that lowercases pre-existing rows; it is collision-safe because the BUNYIP-330 users_email_unique index is already on LOWER(email), and idempotent via a WHERE email <> LOWER(email) guard. Unit-test the helper (mixed-case lowercased, idempotent on already-lowercase). The other three INSERT INTO users sites are non-production e2e/test-only with lowercase-by-construction emails, so the invariant holds workspace-wide. #BUNYIP-325
fix(auth): normalize user emails to lowercase on write
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 26s
Check / fmt + clippy + build + tests (pull_request) Successful in 9m20s
Create release / Create release from merged PR (pull_request) Has been skipped
314c5a01d8
Signing up with a mixed-case email ("Nice.Guy@Example.COM") stored the address verbatim. Lookups already compared with LOWER(email) so login kept working, but the stored address (and the OIDC email claim and the address outbound verification / welcome mail is sent to) diverged in case from what case-sensitive downstream consumers expect, so the verification mail was never reconciled and the account stayed stuck unverified.

Funnel every write to users.email through a new normalize_email() helper (lowercase only, matching the untrimmed LOWER($1) read side) at the two and only write paths: UserRepository::create (the single INSERT choke point that every signup surface inherits) and UserRepository::update_email (the email-change path). Back the stored value with a one-shot backfill migration that lowercases pre-existing rows; it is collision-safe because the BUNYIP-330 users_email_unique index is already on LOWER(email), and idempotent via a WHERE email <> LOWER(email) guard.

Unit-test the helper (mixed-case lowercased, idempotent on already-lowercase). The other three INSERT INTO users sites are non-production e2e/test-only with lowercase-by-construction emails, so the invariant holds workspace-wide.

#BUNYIP-325
Claude-Run deleted branch fix/BUNYIP-325-normalize-email-lowercase 2026-07-04 02:36:44 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!333
No description provided.