Email Claim Policy (primary email & RP sync)
Source:
en/oauth/email-claim.md· Live: https://docs.1pass.dev/en/oauth/email-claim LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).
📍 Jump-to Index
- L41-L52: ## When does email change?
- L53-L105: ## Choose Snapshot or Follow — explicitly
- L61-L96: ### Strategy A — Follow (recommended): adopt changes at the next login
- L97-L105: ### Strategy B — Snapshot: keep the signup-time value, display-only
- L106-L145: ## When email is missing or unverified — the placeholder recipe
- L146-L155: ## Pitfall summary
- L156-L164: ## See also
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. But a multi-platform RP that uses a separate client_id for web/iOS/Android identifies people bycanonical_sub, because the pairwisesubdiverges per client (→ Sub policy).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.
When email is missing or unverified — the placeholder recipe
The RP must still be able to complete login without email scope consent, without the claim present, or with email_verified: false (treating it as absent is always safer than adopting an unverified value). If your local users schema requires email, you have two options:
- Make email nullable — prefer this when possible. It removes the placeholder-maintenance burden entirely.
- Placeholder address — if you can't change the schema, generate one on a reserved domain:
sso-<first 12 chars of sub>@<rp-slug>.invalid.invalid is an RFC 2606 reserved TLD that resolves on no DNS — even an accidental send never leaves your system. Never use a real domain, including your own.
A placeholder is an account state that means "unreachable address." Three rules travel together:
| Rule | Why |
|---|---|
| Exclude from mail sends (suffix filter) | Bounce accumulation damages your sending domain's reputation |
| Apply the Follow strategy — auto-promote once a verified real address arrives | A placeholder must remain a temporary state |
| Block destructive actions where email is the recovery path (e.g. unlinking SSO) | See the warning below |
⚠️ Warning: A password digest the ORM forced in isn't a login method It's common — and harmless on its own — for
has_secure_password-style validations that require a password to auto-fillSecureRandom.hex(16)at SSO signup. The danger is when other code infers "a digest exists, so password login is possible" — the user doesn't know that value, and if the address is a placeholder, a reset email won't reach them either. If that inference makes it into an unlink guard, the user can sever their only login method and get permanently locked out. Apply the unlink guard alongside this.
Pitfall summary
| Pitfall | Consequence | Avoidance |
|---|---|---|
| Keying accounts on email | Primary change splits one person into two accounts | Key on sub (Sub policy) — canonical_sub if multi-platform |
| 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)