배경
앱 환경에서 로그인이 간헐적으로 풀리는 현상. 웹은 미재현.
앱은 토큰 사본이 네이티브 SecureStore + WebView 쿠키 저장소 2곳이고, refresh(rotation)가 4개 경로(앱 부팅 / 웹 axios·SSE / Next 프록시 / iOS 공유 익스텐션)에서 독립적으로 일어나는데, 두 사본의 신선도를 비교하지 않아 죽은 토큰이 살아있는 토큰을 덮어쓰는 시나리오가 생긴다.
서버(piki-server) 실측
- access 15m / refresh 14d / grace 10초 (동시 요청 버스트 흡수용, Redis Lua 원자 처리 — 구현 자체는 문제 없음)
- grace 창 밖에서 옛 refresh 토큰 제시 = 재사용 의심 → family invalidation (유효하던 세션까지 서버에서 즉시 삭제)
- refresh key가
refresh:{userId} — 유저당 1개 (디바이스 구분 없음)
→ 클라이언트가 오래된 토큰으로 refresh를 시도하는 행위 자체가 살아있는 세션을 죽이는 트리거.
발생 시나리오
- 콜드 스타트 로그아웃: 사용 중 프록시(
proxy.ts)가 rotate → 네이티브는 모름(SecureStore stale) → 앱 종료 → 다음 실행 시 부팅 refresh가 stale 토큰 제시 → 서버 family invalidation → 완전 로그아웃 (이후 게스트 자동 로그인으로 덮여 "조용히 풀린" 것처럼 보임)
- 공유 익스텐션 역주입: 익스텐션이 rotate(SecureStore 최신) → 앱 열면 포그라운드 sync가 오래된 쿠키 토큰을 SecureStore에 무조건 덮어씀 → 다음 부팅에서 1번과 동일
- 마운트 레이스:
index.tsx의 mount 시 syncCookies()와 부팅 refresh가 병렬 실행 — 순서에 따라 구 쿠키가 신규 토큰을 덮음
- 세션 쿠키 증발: 프록시 app 분기 Set-Cookie에 Max-Age가 없어 앱 종료 시 최신 토큰 쿠키가 사라짐 (1번을 악화)
클라이언트 수정 (이번 작업)
프록시 rotation의 네이티브 전파는 별도 웹 훅 없이 index.tsx의 AppState background 동기화가 담당 (같은 시점에 네이티브가 직접 쿠키를 읽는 쪽이 웹뷰 JS 생존 여부와 무관해 더 안정적).
배경
앱 환경에서 로그인이 간헐적으로 풀리는 현상. 웹은 미재현.
앱은 토큰 사본이 네이티브 SecureStore + WebView 쿠키 저장소 2곳이고, refresh(rotation)가 4개 경로(앱 부팅 / 웹 axios·SSE / Next 프록시 / iOS 공유 익스텐션)에서 독립적으로 일어나는데, 두 사본의 신선도를 비교하지 않아 죽은 토큰이 살아있는 토큰을 덮어쓰는 시나리오가 생긴다.
서버(piki-server) 실측
refresh:{userId}— 유저당 1개 (디바이스 구분 없음)→ 클라이언트가 오래된 토큰으로 refresh를 시도하는 행위 자체가 살아있는 세션을 죽이는 트리거.
발생 시나리오
proxy.ts)가 rotate → 네이티브는 모름(SecureStore stale) → 앱 종료 → 다음 실행 시 부팅 refresh가 stale 토큰 제시 → 서버 family invalidation → 완전 로그아웃 (이후 게스트 자동 로그인으로 덮여 "조용히 풀린" 것처럼 보임)index.tsx의 mount 시syncCookies()와 부팅 refresh가 병렬 실행 — 순서에 따라 구 쿠키가 신규 토큰을 덮음클라이언트 수정 (이번 작업)
apps/app/utils/jwt.ts) — JWTiat로 두 토큰 중 최신 판별, access 유효성 검사TokenStorage.setTokensIfFresher— 저장된 refresh보다 최신일 때만 저장 (모든 역주입 차단)useWebviewCookieSync) — ① refresh 전에 SecureStore vs 쿠키 중 최신 토큰 채택 ② access가 아직 유효하면 refresh 생략(불필요 rotation 제거) ③ 쿠키 시딩 시 토큰 exp를 expires로 부여(세션 쿠키 방지)index.tsx— syncCookies·WEB_REQ_TOKEN_REFRESHED핸들러에 iat 가드 적용, AppStatebackground전환 시에도 sync(종료 직전 유실 창 축소), mount 중복 sync 제거(레이스 해소)proxy.ts— app 분기 Set-Cookie에 Max-Age 부여 (토큰 JWT의 exp에서 남은 시간을 계산해 사용 — 백엔드 TTL과 자동 일치)useNativeLoginResult의 access 쿠키 만료(15m)와refreshClientToken(1h) 불일치 — 동작엔 무해, 통일만 하면 됨프록시 rotation의 네이티브 전파는 별도 웹 훅 없이
index.tsx의 AppStatebackground동기화가 담당 (같은 시점에 네이티브가 직접 쿠키를 읽는 쪽이 웹뷰 JS 생존 여부와 무관해 더 안정적).