테마
Sub 정책
logi 가 /api/v1/oauth/userinfo 와 id_token 에 emit 하는 identity claim 은 다섯 가지입니다.
| Claim | 타입 | 의미 |
|---|---|---|
sub | string | 이 RP grant 가 처음 발급된 시점의 user.id. 불변(stable). |
canonical_sub | string | 같은 사람의 현재 살아있는(canonical) user.id. 통합 발생 시 변경됨. |
is_canonical | boolean | sub == canonical_sub 의 편의 alias. |
linked_subs | object[] | 이 canonical 에 흡수된 다른 user.id 들의 메타데이터 배열 (자기 자신 제외). 각 원소는 {sub, merged_canonical_sub, merged_via, occurred_at, source_event_id} 형태. 상세 스키마는 아래 참고. |
previously_anonymous | boolean | 이 user 가 익명 가입 후 SSO 로 promoted 된 적이 있는지. |
왜 sub 와 canonical_sub 를 분리했나
OIDC 표준은 sub 가 RP 별로 stable 해야 한다고 명시합니다 (RFC 6749 와 OIDC Core §2). 만약 통합이 발생할 때마다 sub 가 바뀌면:
- RP 가 이전
sub로 저장해둔 외래키가 모두 끊깁니다. - 캐시된 JWT 의
sub가 갑자기 의미를 잃습니다. - OIDC validator 가 토큰 회전 시 reject 할 수 있습니다.
logi 는 다음 규칙을 따릅니다:
sub은 절대 안 바뀝니다. RP grant 가 처음 만들어진 시점의 user.id 가 영구적으로 박힙니다. 그 user 가 나중에 다른 user 로 흡수되더라도 grant 의sub는 그대로.canonical_sub는 통합 시점에 갱신됩니다. RP 는 매 토큰 검증 시 이 값을 보고 "이 user 의 실제 정체성은 누구인가" 를 판단합니다.
따라서 권장 RP 패턴은:
RP DB 의 외래키 = sub (그대로 보존, 마이그레이션 불필요)
런타임 user lookup = canonical_sub 로 한 번 해석 → 실제 user row자세한 마이그레이션 절차는 RP Migration Guide 참고.
linked_subs 의 용도
linked_subs 는 canonical 한 user 에 흡수된 다른 user.id 들의 메타데이터 배열입니다. 각 원소는 단순 string 이 아니라 다음 필드를 가진 object 입니다:
| 필드 | 타입 | 의미 |
|---|---|---|
sub | string | 흡수된 user 의 id (RP 가 과거 외래키로 저장했을 값). |
merged_canonical_sub | string | 흡수 시점의 survivor user.id. 보통 현재 canonical_sub 와 동일. |
merged_via | string | 통합 트리거 종류 (예: "otp", "session_token", "sso_email_match"). |
occurred_at | string | null | 통합 발생 시각 (ISO 8601). |
source_event_id | string | null | 통합을 발생시킨 event_id (audit 추적용). 이벤트 백필 전 통합은 null. |
예를 들어 A → B 통합이 일어난 뒤 user B 의 userinfo 는:
json
{
"sub": "B",
"canonical_sub": "B",
"is_canonical": true,
"linked_subs": [
{
"sub": "A",
"merged_canonical_sub": "B",
"merged_via": "otp",
"occurred_at": "2026-05-11T12:34:56Z",
"source_event_id": "evt_01HE3..."
}
]
}A 가 더 일찍 grant 를 받았던 RP 가 token 을 새로 받으면:
json
{
"sub": "A",
"canonical_sub": "B",
"is_canonical": false,
"linked_subs": []
}여기서 linked_subs 가 빈 배열인 이유는 A 가 흡수된 쪽이지 흡수한 쪽이 아니기 때문입니다. 흡수한 쪽(survivor) 만 linked_subs 에 흡수된 user 들이 들어옵니다.
RP 파싱 주의
linked_subs.forEach(s => map[s] = canonical) 처럼 string 으로 가정해서 처리하면 [object Object] key 가 됩니다. 반드시 link.sub 으로 꺼내 쓰세요:
js
linked_subs.forEach((link) => {
legacyToCanonical[link.sub] = link.merged_canonical_sub;
});RP 는 이 정보를 다음 용도로 씁니다:
- 자기 DB 의 두 row (sub=A, sub=B) 가 logi 입장에서는 같은 사람임을 인식.
- 두 row 의 도메인 데이터를 통합하거나, 적어도 같은 사용자가 양쪽 데이터를 모두 볼 수 있게 권한 분배.
source_event_id와occurred_at으로 자기 audit log 에서 해당 머지 이벤트 추적.
is_canonical 가 false 일 때
이 grant 는 흡수된 user 가 과거에 받은 것입니다. RP 는 두 가지 선택지가 있습니다:
- Canonical 으로 재해석 (권장) —
canonical_sub를 외래키 lookup 의 source 로 사용. RP 의 domain row 가 canonical user 와 연결되어 있다면 그대로 작동. 안 되어 있다면logi_identity_links를 통과한 resolver 가 필요. - 기존 sub 유지 —
sub로 lookup 하고 canonical 변경에 무관심. 단, 같은 사람의 다른 grant 가 들어왔을 때 두 row 로 인식하게 됨.
대부분의 RP 는 1번 패턴을 채택했고, logi 의 Rails/Swift/Kotlin SDK 도 1번을 기본값으로 가정합니다.
previously_anonymous
익명-우선 가입 흐름(v0.4)에서 user 가 처음에 익명 row 로 만들어지고 나중에 SSO 가 붙으면 previously_anonymous: true 로 마킹됩니다. 이 값은:
- 사용자가 자기 데이터에 대해 "익명 시절 데이터 포함" 여부를 인지하도록 RP UI 에 노출 가능.
- GDPR/개인정보처리방침 상 익명-promoted user 와 처음부터 식별된 user 를 구분할 필요가 있을 때 사용.
previously_anonymous 는 한 번 true 가 되면 false 로 돌아가지 않습니다.
OIDC 호환성 caveat
순수 OIDC validator (예: Auth0 SDK, Keycloak 토큰 검사기) 는 canonical_sub / linked_subs 같은 logi-specific claim 을 모릅니다. 이는 의도된 동작 입니다 — 추가 claim 은 OIDC 표준의 "additional claims" 영역에 들어가며, 표준 validator 는 무시하고 sub 만 봅니다. 통합 직후 표준 validator 가 같은 사람을 두 row 로 보더라도 이는 RP 가 logi-specific claim 을 처리할 때까지의 일시적 갭이며, 다음 토큰 회전 또는 polling 동기화 후 정정됩니다.
Pairwise sub — RP 간 격리 보장
sub 는 OauthApplication 별로 다르게 발급됩니다 (OIDC §8.1 Pairwise Subject Identifier). 같은 logi user 가 두 RP 에 동시 로그인해도 RP A 와 RP B 가 받는 sub 는 서로 다른 값. 두 RP 가 사용자 데이터를 합쳐도 같은 사람인지 식별 불가 (correlation 공격면 차단).
구현: oauth_applications.pairwise_salt (앱별 48-byte 랜덤 솔트) + HMAC.
함정: 같은 RP 의 web client 와 mobile client 는 별개 OauthApplication 이므로 서로 다른 sub 를 받습니다. RP 측에서 한 사용자가 두 row 로 보이는데 identity_links 로 묶을 수 있습니다.
회귀 방지 spec: spec/integrations/krx_listing_rp_integration_spec.rb#"issues distinct pairwise-sub per RP for the same user".