Skip to content

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.

ClaimTypeMeaning
emailstringThe primary email at request time. Mutable — the user can change it in logi settings at any time.
email_verifiedbooleanWhether the primary email comes from a verified source (verified credential / Apple / Google SSO).

Three core rules:

  1. email is not an identifier. Key your accounts on sub. The same user may arrive with a different email on their next login. But a multi-platform RP that uses a separate client_id for web/iOS/Android identifies people by canonical_sub, because the pairwise sub diverges per client (→ Sub policy).
  2. id_token carries no email claim. Call userinfo when you need the email.
  3. 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 is email_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.

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.

ruby
# 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}")
end

Caveats:

  • 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:

  1. Make email nullable — prefer this when possible. It removes the placeholder-maintenance burden entirely.
  2. Placeholder address — if you can't change the schema, generate one on a reserved domain:
text
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:

RuleWhy
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 arrivesA 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-fill SecureRandom.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

PitfallConsequenceAvoidance
Keying accounts on emailPrimary change splits one person into two accountsKey on sub (Sub policy) — canonical_sub if multi-platform
Implicit Snapshot"I changed my primary but the RP didn't update" bug reportsAdopt Follow, or document Snapshot
Adopting unverified emailImpersonation → account-link takeoverConsume only when email_verified == true
Follow applied to AppleRelay address overwrites the real emailFollow is one_pass-only
Refresh failure breaks loginConcurrency race turns login into a 500Refresh is best-effort; rescue and proceed

See also

최종 수정:

Identity가 제품의 신뢰를 만듭니다.