테마
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_token | access_token 재발급 | 30일 (rotation) |
| device_secret | 익명 user 의 1차 PoP, device credential | 무기한 (디바이스 lifecycle) |
저장 패턴은 Android 통합 — Refresh Token 저장 의 DataStore + Tink 패턴을 따르되 두 비밀을 별도 DataStore 키 (alias) 로 분리합니다 — 한 쪽을 revoke/wipe 할 때 다른 쪽에 영향이 없도록.
EncryptedSharedPreferences 금지
androidx.security:security-crypto 의 EncryptedSharedPreferences 는 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/mergevia WebView) 실행 후 다음 토큰 회전에서 canonical 이 바뀜. - [ ] 익명-우선 가입 + SSO promotion 후
previously_anonymous == true. - [ ] 흡수된 쪽의 refresh token 이 즉시 invalid (
refresh_token_revoked응답).