Skip to content

Sub 정책

logi 가 /api/v1/oauth/userinfoid_token 에 emit 하는 identity claim 은 다섯 가지입니다.

Claim타입의미
substring이 RP grant 가 처음 발급된 시점의 user.id. 불변(stable).
canonical_substring같은 사람의 현재 살아있는(canonical) user.id. 통합 발생 시 변경됨.
is_canonicalbooleansub == canonical_sub 의 편의 alias.
linked_subsobject[]이 canonical 에 흡수된 다른 user.id 들의 메타데이터 배열 (자기 자신 제외). 각 원소는 {sub, merged_canonical_sub, merged_via, occurred_at, source_event_id} 형태. 상세 스키마는 아래 참고.
previously_anonymousboolean이 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 입니다:

필드타입의미
substring흡수된 user 의 id (RP 가 과거 외래키로 저장했을 값).
merged_canonical_substring흡수 시점의 survivor user.id. 보통 현재 canonical_sub 와 동일.
merged_viastring통합 트리거 종류 (예: "otp", "session_token", "sso_email_match").
occurred_atstring | null통합 발생 시각 (ISO 8601).
source_event_idstring | 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_idoccurred_at 으로 자기 audit log 에서 해당 머지 이벤트 추적.

is_canonical 가 false 일 때

이 grant 는 흡수된 user 가 과거에 받은 것입니다. RP 는 두 가지 선택지가 있습니다:

  1. Canonical 으로 재해석 (권장) — canonical_sub 를 외래키 lookup 의 source 로 사용. RP 의 domain row 가 canonical user 와 연결되어 있다면 그대로 작동. 안 되어 있다면 logi_identity_links 를 통과한 resolver 가 필요.
  2. 기존 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 간 격리 보장

subOauthApplication 별로 다르게 발급됩니다 (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 clientmobile 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".

참고

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