Skip to content

TODO (IA — follow-up)

이 문서와 Android 통합 의 정보 아키텍처가 겹칩니다. 편집 결정 후 둘 중 하나를 canonical 로 정리할 예정. 현재는 이 문서를 Account Merge / canonical_sub addendum 으로 사용하세요. OAuth/저장소 기본 패턴은 Android 통합 을 따르세요.

Android (Kotlin) 통합

이 문서는 Android 네이티브 앱(Jetpack Compose / View XML 무관) 에서 logi 를 RP 로 통합하는 표준 패턴을 다룹니다. iOS Swift 가이드 (Swift) 의 Android 미러이며, OAuth 흐름과 자격증명 보관에서 같은 약속을 제공합니다.

전반적 아키텍처 권장 사항은 Android 통합 페이지에 더 자세히 있고, 이 문서는 canonical_sub / linked_subs 처리자격증명 분리 보관 패턴에 중점을 둡니다.

자격증명 보관

logi 가 발급하는 두 가지 클라이언트-side 비밀:

비밀용도만료
refresh_tokenaccess_token 재발급30일 (rotation)
device_secret익명 user 의 1차 PoP, device credential무기한 (디바이스 lifecycle)

저장 패턴은 Android 통합 — Refresh Token 저장DataStore + Tink 패턴을 따르되 두 비밀을 별도 DataStore 키 (alias) 로 분리합니다 — 한 쪽을 revoke/wipe 할 때 다른 쪽에 영향이 없도록.

EncryptedSharedPreferences 금지

androidx.security:security-cryptoEncryptedSharedPreferences 는 1.1.0 부터 deprecated. 신규 코드에서는 DataStore + Tink 패턴만 사용하세요. device_secret 도 절대 plain SharedPreferences 에 저장 금지 — 백업에 평문 노출됩니다.

Claims 모델

kotlin
@Serializable
data class LogiClaims(
    val sub: String,
    @SerialName("canonical_sub") val canonicalSub: String? = null,
    @SerialName("is_canonical") val isCanonical: Boolean? = null,
    @SerialName("linked_subs") val linkedSubs: List<String>? = null,
    @SerialName("previously_anonymous") val previouslyAnonymous: Boolean? = null,
    val email: String? = null,
    @SerialName("email_verified") val emailVerified: Boolean? = null,
    val anonymous: Boolean? = null
) {
    /** 외래키 / 로컬 캐시 lookup 시 항상 이 값을 쓴다. */
    val effectiveSub: String
        get() = canonicalSub ?: sub
}

토큰 검증 후 user 매핑

kotlin
class LogiSessionHandler(private val localStore: LocalStore) {
    fun handle(claims: LogiClaims) {
        val canonical = claims.effectiveSub
        val currentLocal = localStore.userId
        if (currentLocal != null && currentLocal != canonical) {
            // 통합 발생 — 로컬 캐시를 canonical 로 갱신.
            localStore.migrateUser(from = currentLocal, to = canonical)
        }
        localStore.userId = canonical

        if (claims.previouslyAnonymous == true) {
            localStore.previouslyAnonymous = true
        }
    }
}

linked_subs 처리

흡수한 쪽 (isCanonical == true) 이 갖고 있던 옛 sub 들이 있다면, 로컬 캐시의 데이터를 canonical sub view 로 끌어옵니다:

kotlin
if (claims.isCanonical == true) {
    claims.linkedSubs?.forEach { absorbed ->
        localStore.reassignData(from = absorbed, to = claims.sub)
    }
}

reassignData 는 idempotent 하게 작성합니다 — 이미 옮겨진 sub 는 skip.

Anonymous → Identified 전환 감지

kotlin
if (claims.anonymous == false && localStore.wasAnonymous) {
    // promotion 발생: 익명 시절 device-local 데이터를
    // promoted user 의 cloud 계정으로 sync.
    syncEngine.runPromotionUpload()
    localStore.wasAnonymous = false
}

OAuth 흐름

Authorization Code + PKCE 흐름은 Android 페이지의 표준 패턴을 그대로 사용합니다. 이 문서가 다루는 것은 그 위에 얹히는 canonical 인식 layer 입니다 — OAuth 자체의 redirect/state/code_verifier 처리는 기존 가이드 참조.

통합 동작 검증

production flip 전 다음을 staging 에서 확인:

  • [ ] 두 SSO 가 같은 디바이스에서 발생했을 때 (T1) 다음 토큰의 canonical_sub 가 적절히 채워짐.
  • [ ] 같은 이메일의 SSO 로 처음 가입했을 때 (T2) linked_subs 가 즉시 들어옴.
  • [ ] T3 (/me/merge via WebView) 실행 후 다음 토큰 회전에서 canonical 이 바뀜.
  • [ ] 익명-우선 가입 + SSO promotion 후 previously_anonymous == true.
  • [ ] 흡수된 쪽의 refresh token 이 즉시 invalid (refresh_token_revoked 응답).

관련 문서

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