Email Claim Policy
The email emitted by /oauth/userinfo is the user's primary email at the time of the request. It is not the email the user typed to log in, and it is not a value frozen for your RP forever.
| Claim | Type | Meaning |
|---|---|---|
email | string | The primary email at request time. Mutable — the user can change it in logi settings at any time. |
email_verified | boolean | Whether the primary email comes from a verified source (verified credential / Apple / Google SSO). |
Three core rules:
emailis not an identifier. Key your accounts onsub. The same user may arrive with a differentemailon their next login.id_tokencarries no email claim. Call userinfo when you need the email.- Login email ≠ email claim. A logi account can hold several login emails (legacy email_address, Apple/Google/Kakao SSO emails, verified extra emails); whichever one the user logs in with, userinfo always emits the primary email.
When does email change?
- The user changes their primary email in the logi app (Settings → Account → Email).
- Accounts that sign up via SSO (Apple/Google) get their primary email explicitly pinned to that SSO email at signup (policy since 2026-06-11). After that, only an explicit pick changes it — adding another email never silently flips what your RP receives.
- Apple Hide-My-Email users may present a relay address (
@privaterelay.appleid.com). Apple guarantees delivery, so it isemail_verified: true.
Choose Snapshot or Follow — explicitly
Most OAuth sample code stores the email once, at account creation. That silently makes you a Snapshot RP: when the user later changes their primary email in logi, your app keeps showing the signup-time address forever — and users report that as a bug. Either strategy is legitimate; make the choice visible in code and docs.
Strategy A — Follow (recommended): adopt changes at the next login
On every SSO login, if the verified email differs from your stored value, update it. This is login-time adoption, not background sync — no extra infrastructure needed.
# Right after resolving the existing user by sub (Rails example)
def refresh_email_if_changed!(user)
return unless @email.present? && @email_verified
return if user.email_address == @email
# Another local account already owns the address — skip, but let login proceed
if User.where.not(id: user.id).where(email_address: @email).exists?
Rails.logger.warn("[sso] email refresh skipped: collision user=#{user.id}")
return
end
user.update!(email_address: @email)
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
# TOCTOU race with a concurrent signup — refresh is best-effort; never fail the login
Rails.logger.warn("[sso] email refresh skipped: race user=#{user.id}")
endCaveats:
- Scope this to the logi (one_pass) provider only. Applying the same pattern to Apple logins lets Hide-My-Email relay addresses overwrite real emails.
- Never adopt a value with
email_verified: false(account-takeover vector). - If your local email doubles as the password-login identifier, the user will log in locally with the new email + existing password after a refresh. That is the intended "IdP is the source of truth" behavior, but consider telling the user.
Strategy B — Snapshot: keep the signup-time value, display-only
If you treat email purely as contact/display data and let users manage it inside your RP, Snapshot is fine. But:
- Leave a comment saying the non-sync is intentional.
- Provide an email-edit UI in your RP, or at least make it clear that changing the email in logi will not propagate here.
Pitfall summary
| Pitfall | Consequence | Avoidance |
|---|---|---|
| Keying accounts on email | Primary change splits one person into two accounts | Key on sub (Sub policy) |
| Implicit Snapshot | "I changed my primary but the RP didn't update" bug reports | Adopt Follow, or document Snapshot |
| Adopting unverified email | Impersonation → account-link takeover | Consume only when email_verified == true |
| Follow applied to Apple | Relay address overwrites the real email | Follow is one_pass-only |
| Refresh failure breaks login | Concurrency race turns login into a 500 | Refresh is best-effort; rescue and proceed |
See also
- Sub policy (sub / canonical_sub) — always key on sub
- Recommended architecture (RP integration)
- Scopes — the
emailscope and per-claim consent - Event Delivery (3-tier) — future room for webhook/polling if you want changes without a login (no email-change event is emitted today)