Skip to content

Email Claim 정책

/oauth/userinfo 가 emit 하는 email그 시점 사용자의 대표 이메일(primary email) 입니다. 사용자가 로그인할 때 입력한 이메일이 아니며, RP 에 영구히 고정되는 값도 아닙니다.

Claim타입의미
emailstring요청 시점의 대표 이메일. 가변(mutable) — 사용자가 logi 설정에서 언제든 변경 가능.
email_verifiedboolean대표 이메일이 검증된 소스(verified credential / Apple / Google SSO)에서 왔는지.

핵심 규칙 세 가지:

  1. email 은 식별자가 아닙니다. 계정 키는 반드시 sub 를 쓰세요. 같은 사용자가 다음 로그인에서 다른 email 을 들고 올 수 있습니다.
  2. id_token 에는 email claim 이 없습니다. email 이 필요하면 userinfo 를 호출하세요.
  3. 로그인에 쓴 이메일 ≠ email claim. logi 계정에는 여러 로그인 이메일(legacy email_address, Apple/Google/Kakao SSO 이메일, verified 추가 이메일)이 붙을 수 있고, 어느 것으로 로그인하든 userinfo 는 항상 대표 이메일을 내보냅니다.

email 은 언제 바뀌나

  • 사용자가 logi 앱 설정 → 계정 → 이메일 에서 대표 이메일을 변경했을 때.
  • SSO(Apple/Google)로 가입한 계정은 가입 순간 대표 이메일이 해당 SSO 이메일로 명시적으로 고정됩니다 (2026-06-11 정책). 이후 변경은 위의 명시적 선택뿐 — 이메일을 추가했다고 해서 RP 에 나가는 값이 소리 없이 바뀌지 않습니다.
  • Apple Hide-My-Email 사용자는 relay 주소(@privaterelay.appleid.com)가 올 수 있습니다. Apple 이 전달을 보증하므로 email_verified: true 입니다.

RP 는 Snapshot 과 Follow 중 하나를 명시적으로 선택하세요

대부분의 OAuth 예제 코드는 email 을 계정 생성 시 한 번만 저장합니다. 그 결과 아무 결정 없이 암묵적으로 Snapshot 이 되고, 사용자가 logi 에서 대표 이메일을 바꿔도 RP 화면에는 영원히 가입 시점 이메일이 남습니다 — 사용자는 이걸 버그로 인식합니다. 어느 쪽이든 의도를 코드와 문서에 남기세요.

전략 A — Follow (권장): 다음 로그인부터 IdP 를 따라간다

매 SSO 로그인 시 email_verified: true 인 email 이 로컬 저장값과 다르면 갱신합니다. 백그라운드 동기화가 아니라 로그인 시점 반영이므로 추가 인프라가 필요 없습니다.

ruby
# sub 매칭으로 기존 user 를 찾은 직후 (예: Rails)
def refresh_email_if_changed!(user)
  return unless @email.present? && @email_verified
  return if user.email_address == @email

  # 다른 로컬 계정이 이미 그 주소를 쓰면 갱신을 건너뛴다 (로그인은 계속 진행)
  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 — 갱신은 best-effort, 로그인은 깨지 않는다
  Rails.logger.warn("[sso] email refresh skipped: race user=#{user.id}")
end

주의사항:

  • logi(one_pass) provider 에만 적용하세요. Apple 로그인에 같은 패턴을 쓰면 Hide-My-Email relay 주소가 실제 이메일을 덮어쓸 수 있습니다.
  • email_verified: false 인 값은 절대 채택하지 마세요 (계정 탈취 벡터).
  • 로컬 이메일이 비밀번호 로그인 식별자를 겸한다면, 갱신 후 사용자는 새 이메일 + 기존 비밀번호로 로컬 로그인하게 됩니다. IdP 가 source of truth 라는 의도된 동작이지만, 사용자 안내가 필요할 수 있습니다.

전략 B — Snapshot: 가입 시점 값을 표시 전용으로 보존

email 을 연락·표시 용도로만 쓰고 사용자가 RP 안에서 직접 관리하게 한다면 Snapshot 도 정당합니다. 단:

  • 코드에 "의도적으로 동기화하지 않는다" 주석을 남기세요.
  • RP 안에 이메일 수정 UI 를 제공하거나, 최소한 "logi 에서 바꿔도 여기 반영되지 않는다" 를 사용자가 알 수 있게 하세요.

함정 정리

함정결과회피
email 로 계정 키잉대표 변경 시 같은 사람이 새 계정으로 분리sub 키잉 (Sub 정책)
암묵적 Snapshot"대표 바꿨는데 RP 에 반영 안 됨" 사용자 버그 리포트Follow 채택 또는 Snapshot 명시
unverified email 채택타인 이메일 사칭 → 계정 연결 탈취email_verified == true 일 때만 소비
Apple 에 Follow 적용relay 주소가 실이메일 덮어씀Follow 는 one_pass 전용
갱신 실패가 로그인 중단동시성 race 로 로그인 500갱신은 best-effort, rescue 후 진행

참고

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