Skip to content

RP Migration Guide (canonical_sub)

Source: oauth/rp-migration-guide.md · Live: https://docs.1pass.dev/oauth/rp-migration-guide LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).

📍 Jump-to Index

  • L33-L63: ## 전체 그림
  • L64-L96: ## 5단계 deploy 순서
    • L68-L73: ### Phase 1 — 스키마 + 비활성 코드
    • L74-L79: ### Phase 2 — webhook receiver 활성화
    • L80-L84: ### Phase 3 — polling reconciler 활성화
    • L85-L89: ### Phase 4 — login-time resolver 활성화
    • L90-L96: ### Phase 5 — enforce_canonical_resolution = true flip
  • L97-L162: ## Rails 기준 코드 패턴
    • L99-L124: ### Concern 예시
    • L125-L138: ### Pundit policy 주의
    • L139-L162: ### Merge Reconciler
  • L163-L168: ## 무엇이 안 바뀌나
  • L169-L174: ## 무엇이 바뀌나
  • L175-L188: ## 검증 체크리스트
  • L189-L195: ## 관련 문서

RP Migration Guide — canonical_sub 도입

이 가이드는 이미 logi 와 연동된 RP 가 기존 sub 외래키를 유지한 채 canonical_sub 기반 user 해석으로 안전하게 전환하는 절차를 다룹니다. logi 가 첫 통합 트리거를 production 에 켜기 전에 RP-side 작업이 완료되어 있어야 합니다.

💡 Tip: 멀티플랫폼 RP 는 처음부터 canonical_sub 로 식별 웹·iOS·Android 를 별개 client_id 로 등록했다면, sub 가 client 별 pairwise 로 갈리므로 마이그레이션 이전에도 사람 단위 식별은 canonical_sub 로 해야 합니다. 이 경우 외래키 자체를 canonical_sub 로 잡으면 아래 resolver hop 도 단순해집니다. 배경과 증상(AlreadyLinkedError)은 Sub 정책 — 멀티-client RP 식별 참고.

전체 그림

기존 RP DB 는 다음과 같이 logi user 를 참조하고 있습니다:

users (RP own table)
  logi_sub  string  unique   ← logi 의 user.id

마이그레이션 후:

users (그대로 유지)
  logi_sub  string  unique

logi_identity_links (신규)
  primary_user_id  string  ← canonical_sub
  linked_user_id   string  unique  ← 흡수된 sub
  merged_via       string
  occurred_at      datetime

RP 의 모든 user lookup 은 canonical resolver 를 통과합니다:

ruby
def find_canonical_user(sub)
  canonical = LogiIdentityLink.find_by(linked_user_id: sub)&.primary_user_id || sub
  User.find_by(logi_sub: canonical)
end

5단계 deploy 순서

logi 와 협조하는 joint deploy 입니다. 각 단계는 다음 단계 전에 24h soak 권장.

Phase 1 — 스키마 + 비활성 코드

  • logi_identity_links 테이블 추가 + 인덱스
  • canonical resolver 코드 추가 (사용 안 함, feature flag enforce_canonical_resolution = false)
  • existing user lookup path 는 그대로 logi_sub 직접 사용

Phase 2 — webhook receiver 활성화

  • /webhooks/logi endpoint 활성화 → user.merged 수신 시 logi_identity_links row INSERT
  • HMAC 검증 + event_id dedupe 구현
  • 이 시점에는 logi 가 아직 통합을 발화하지 않으므로 row 가 들어오지 않음 — receiver 의 코드 경로 자체가 dry-run

Phase 3 — polling reconciler 활성화

  • 분 단위 GET /api/v1/events?since=<cursor> polling job
  • cursor 를 자기 DB 에 저장 (transaction 안에서 cursor advance + event apply)

Phase 4 — login-time resolver 활성화

  • BearerAuthentication (또는 동등) concern 이 매 token 검증 시 canonical_sub 를 보고 logi_identity_links 와 비교, 차이가 있으면 즉시 row insert
  • 이는 webhook/polling 누락의 마지막 안전망

Phase 5 — enforce_canonical_resolution = true flip

  • 모든 user lookup 이 canonical resolver 를 통과하도록 변경
  • 기존 User.find_by(logi_sub:) 직접 호출이 코드베이스에 남아 있는지 grep 으로 확인 (bin/rails runner + custom audit 권장)

flip 후에야 logi 가 통합 트리거를 production 에서 켭니다.

Rails 기준 코드 패턴

Concern 예시

ruby
# app/controllers/concerns/logi_canonical_resolution.rb
module LogiCanonicalResolution
  extend ActiveSupport::Concern

  private

  def resolve_logi_user(sub:, canonical_sub:, linked_subs: [])
    # 1) login-time fallback: webhook/polling 누락 시 여기서 보정
    if canonical_sub != sub && !LogiIdentityLink.exists?(linked_user_id: sub)
      LogiIdentityLink.create!(
        primary_user_id: canonical_sub,
        linked_user_id:  sub,
        merged_via:      "login_time_fallback",
        occurred_at:     Time.current
      )
    end

    # 2) 항상 canonical 으로 lookup
    User.find_by(logi_sub: canonical_sub)
  end
end

Pundit policy 주의

Pundit policy 가 record.user_id == current_user.id 같은 직접 비교를 한다면, 통합 후 같은 사람이지만 두 row 가 다른 logi_sub 를 가진 케이스에서 권한이 빠질 수 있습니다. 권장 패턴:

ruby
class ApplicationPolicy
  def same_canonical?(record_user)
    LogiCanonical.same_owner?(current_user, record_user)
  end
end

LogiCanonical.same_owner? 는 두 user 의 logi_sub 를 각각 canonical 으로 resolve 한 뒤 비교. 자세한 patterns 는 Rails 통합 가이드 참고.

Merge Reconciler

webhook 또는 polling 으로 user.merged 가 들어오면:

ruby
class MergeReconciler
  def apply(event)
    survivor = event[:data][:survivor_canonical_sub]
    merged   = event[:data][:merged_sub]

    LogiIdentityLink.create_or_find_by!(linked_user_id: merged) do |link|
      link.primary_user_id = survivor
      link.merged_via      = event[:data][:merged_via]
      link.occurred_at     = Time.parse(event[:data][:triggered_at])
    end

    # 도메인 데이터의 logical merge 는 RP 정책에 따라.
    # 예: merged 의 active session 강제 종료, 두 user 의 주문 history 합치기, etc.
  end
end

create_or_find_by! 가 idempotent 를 보장하므로 같은 webhook 이 두 번 와도 안전.

무엇이 안 바뀌나

  • 기존 logi_sub 외래키는 그대로 유지. row 의 logi_sub 가 마이그레이션으로 바뀌지 않습니다.
  • 새로 가입한 user 의 logi_sub 도 그대로. sub 은 OIDC 상 stable.
  • RP 의 외부 API 응답 — user 식별자를 외부에 노출했다면 그 값도 그대로.

무엇이 바뀌나

  • 통합이 일어난 사용자가 새 토큰을 받을 때 canonical_subsub 과 다름.
  • linked_subs 가 채워질 수 있음.
  • 모든 RP-side user lookup 이 canonical resolver 한 hop 을 거쳐야 함.

검증 체크리스트

flip 전:

  • [ ] LogiIdentityLink 테이블 + UNIQUE(linked_user_id) 인덱스
  • [ ] HMAC + event_id dedupe 구현된 webhook receiver
  • [ ] cursor 기반 polling reconciler (분 단위 권장)
  • [ ] login-time canonical resolver
  • [ ] User.find_by(logi_sub:) 직접 호출 모두 canonical resolver 경유로 교체
  • [ ] Pundit policy / scope 가 canonical 비교 사용
  • [ ] feature flag enforce_canonical_resolution = true 로 flip 준비

logi 운영자에게 flip 완료를 통보한 뒤 logi 가 T1 트리거를 production 에 enable 합니다.

관련 문서

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