Auto-refresh 동작
SDK 의 setupLearningSdk() 가 자동으로 처리. Frontend 가 명시 호출할 일 없음.
작동 방식
1. 일반 호출 — Request interceptor 가 Authorization 자동 주입
[SDK 호출]
→ Request interceptor: hooks.getAccessToken() 의 token 을 Authorization 헤더에 자동
→ fetch
→ 200 응답
2. 만료 — Response interceptor 가 자동 refresh + retry
[SDK 호출]
→ 401 응답 (access token 만료)
→ Response interceptor: refresh promise 시작 (singleton — 동시 401 N개 = 1 refresh)
→ POST /client/auth/refresh-token (cookie 자동 동봉, credentials: 'include')
→ 새 accessToken 받음 → hooks.setAccessToken 호출
→ 원 request 재시도 (1회)
→ 200 응답
동시에 401 받은 N 개 호출이 있어도 refresh 는 1회만 — 나머지는 같은 promise 공유.
3. Multi-tab — Web Locks API
같은 origin 의 여러 tab 이 동시에 만료된 access token 가지고 있을 때:
Tab A: 401 → Web Lock 'learning-sdk-refresh' 획득 → refresh API 호출
Tab B: 401 → 같은 Lock 대기
Tab A: refresh 성공 → BroadcastChannel('learning-sdk-auth') 로 새 token 전파
Tab B: BroadcastChannel 메시지 받음 + Lock 해제 → memory token 갱신 후 원 request 재시도
→ refresh API 가 한 번만 호출됨. multi-tab race 없음.
4. Refresh 실패 — onAuthFailure
[refresh API 호출]
→ 401 (refresh token 만료 / reuse 감지 / revoked family)
→ SDK 가 hooks.onAuthFailure() 호출
→ frontend 가 sign-in 페이지로 redirect
Reuse detection — 보안 안전망
정상: token A → rotate → token B (A 의 consumedAt set)
→ rotate → token C (B 의 consumedAt set)
...
도난 시나리오 (A 도난):
- 정상 사용자: A → rotate → B (정상 흐름)
- 공격자도 A 가지고 있음 → A 재사용 시도 → 백엔드 reuse 감지 → family 전체 revoke
- 정상 사용자도 다음 refresh 시 401 받지만, 공격자도 끊김.
- 정상 사용자가 다시 sign-in → 새 family. 공격자는 다시 도난 안 하는 한 끊김.
frontend 측에서 특별히 처리할 것 없음. 백엔드 + SDK 자동.
Proactive refresh — 만료 직전 미리
setupLearningSdk 의 getAccessTokenExpiresAt hook 을 제공하면:
[SDK 호출]
→ Request interceptor: 만료까지 30초 미만이면 background 로 refresh 호출
→ 새 token 으로 outgoing request 의 Authorization 헤더 갱신
→ fetch
→ 401 후 retry 비용 줄임 (정상 흐름이 한 번에 200).
비활성: setupLearningSdk({ proactiveRefreshLeadMs: 0, ... }).
Retry — 5xx / 408 / 429 / network
[SDK 호출]
→ 503 또는 network error
→ Error interceptor: exponential backoff (1s → 2s → 4s, jitter ±250ms, max 10s)
→ 최대 3회 재시도
→ 모두 실패 시 caller 로 throw
비활성 / 옵션 조정:
setupLearningSdk({ maxRetries: 0 }) // retry 안 함
setupLearningSdk({ maxRetries: 5 }) // 5회까지
setupLearningSdk({ timeoutMs: 0 }) // timeout 비활성
setupLearningSdk({ timeoutMs: 60000 }) // 60초로
Sign-out
await ClientAuth.signOut({}) // 현재 device 의 refresh family revoke + cookie clear
await ClientAuth.signOutAll({}) // user 의 모든 device revoke
sessionStorage.removeItem('app.access')
location.href = '/sign-in'