Token Refresh That Never Logs Users Out
15 January 2025
A single-flight refresh guard, retry strategy, and clock-skew handling so users stay signed in.
TL;DR
Use a central session manager with a single-flight refresh guard: only one in-flight refresh at a time, queue other requests until it completes. Retry with exponential backoff and handle clock skew. Store tokens in Keystore/Secure Enclave. On 401, attempt refresh once; if refresh fails, then logout. Never log users out on a single failed request.
Architecture
One SessionManager (or AuthRepository) owns the current session. All API calls go through an interceptor that attaches the access token. If the interceptor sees 401, it calls SessionManager.refresh(). Refresh() uses a mutex or single promise so concurrent callers wait on the same refresh. After refresh, retry the original request. Tokens live in Keystore; in-memory cache is optional for speed. Logout clears Keystore and in-memory state.
Failure modes
- Refresh loop: refresh token expired or revoked but client keeps retrying.
- Clock skew: device time wrong, token appears expired when it isn’t (or vice versa).
- Token leakage: tokens in logs, crash reports, or screenshots.
- Logout not clearing all state: remnants in storage or memory.
Testing checklist
- Unit tests: refresh called once under 401; concurrent requests share one refresh.
- Mock server: 401 → refresh → 200; 401 → refresh fails → logout.
- Clock skew: advance device time, confirm token validation still correct.
- Snapshot or integration test: logout clears storage and in-memory session.
What I'd do differently next time
I’d add a small “session health” ping (e.g. every N minutes) that validates the refresh token server-side and proactively refreshes before expiry, so we rarely hit the expiry edge at all.