fix: only link OIDC logins by email when the provider verified it #37

Merged
jfro merged 2 commits from jtk/oidc-email-linking into main 2026-10-18 17:45:40 +00:00
Owner

Closes #11.

Problem: when no user matched the OIDC provider + sub, the callback logged in whichever OIDC user had the same email, with no email_verified check. On an IdP where users can set their own email, that's account takeover.

Fix:

  • Users are matched on provider + sub first, as before.
  • An existing OIDC account with the same email but a different sub is linked only when the provider sends email_verified: true (boolean or "true"). Otherwise the login is refused with the existing "account with this email already exists" message.
  • Local (password) accounts are still never linked.
  • The lookup/creation moved from the controller into Accounts.find_or_create_oidc_user/2 so it can be unit tested. The controller just passes the claims.
  • Logging: info when a login is linked by verified email, warning when one is refused (user id only, no tokens).

Tests: 5 new tests in accounts_test.exs (new user, same sub, unverified/false refused, verified true/"true" linked, local account never linked). mix precommit: 487 passed.

Note: this touches the same handle_user_creation/… function as #34. I'll merge main in after #34 lands.

Closes #11. **Problem:** when no user matched the OIDC provider + `sub`, the callback logged in whichever OIDC user had the same email, with no `email_verified` check. On an IdP where users can set their own email, that's account takeover. **Fix:** - Users are matched on provider + `sub` first, as before. - An existing OIDC account with the same email but a different `sub` is linked only when the provider sends `email_verified: true` (boolean or `"true"`). Otherwise the login is refused with the existing "account with this email already exists" message. - Local (password) accounts are still never linked. - The lookup/creation moved from the controller into `Accounts.find_or_create_oidc_user/2` so it can be unit tested. The controller just passes the claims. - Logging: info when a login is linked by verified email, warning when one is refused (user id only, no tokens). **Tests:** 5 new tests in `accounts_test.exs` (new user, same sub, unverified/false refused, verified true/"true" linked, local account never linked). `mix precommit`: 487 passed. Note: this touches the same `handle_user_creation/…` function as #34. I'll merge main in after #34 lands.
fix: only link OIDC logins by email when the provider verified it
All checks were successful
CI / test (pull_request) Successful in 55s
59e1c2b8c6
Match OIDC users on provider + sub. An existing OIDC account with the same
email but a different sub is linked only when email_verified is true.
Moves the lookup into Accounts.find_or_create_oidc_user/2 so it's tested.

Closes #11
Merge remote-tracking branch 'origin/main' into jtk/oidc-email-linking
All checks were successful
CI / test (pull_request) Successful in 42s
f60f5c8daf
jfro left a comment

Review from Nye: APPROVE (posted by Upper Wolfjaw; Nye's token is read-only)

CI green, mergeable into main 4104ba4. Checked: match on provider+sub first; same-email OIDC account with a different sub is linked only when email_verified is true/"true"; local (provider nil) accounts are never linked; the email column is citext so case variants can't bypass; the disabled-user check still runs after lookup; tests cover all 5 paths.

Non-blocking low (filed separately): handle_successful_auth falls back to user_info["mail"] when "email" is missing, but email_verified only vouches for the "email" claim, so the linking path should only trust email_verified when the email came from "email". Also the :email_taken warning logs the raw email (PII, same family as #15).

**Review from Nye: APPROVE** (posted by Upper Wolfjaw; Nye's token is read-only) CI green, mergeable into main 4104ba4. Checked: match on provider+sub first; same-email OIDC account with a different sub is linked only when email_verified is true/"true"; local (provider nil) accounts are never linked; the email column is citext so case variants can't bypass; the disabled-user check still runs after lookup; tests cover all 5 paths. Non-blocking low (filed separately): handle_successful_auth falls back to user_info["mail"] when "email" is missing, but email_verified only vouches for the "email" claim, so the linking path should only trust email_verified when the email came from "email". Also the :email_taken warning logs the raw email (PII, same family as #15).
jfro merged commit ab47811f93 into main 2026-10-18 17:45:40 +00:00
jfro deleted branch jtk/oidc-email-linking 2026-10-18 17:45:40 +00:00
Sign in to join this conversation.
No reviewers
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
jfro/fuzzy_catalog!37
No description provided.