Authentication & Token Lifecycle
The mobile app authenticates with the backend using a JWT access/refresh token pair issued by Django SimpleJWT. This document covers token lifetimes, the proactive renewal strategy, and how the app handles failures to avoid spurious logouts.
Token Lifetimes
| Token | Lifetime | Notes |
|---|---|---|
| Access token | 30 minutes | Used in the Authorization: Bearer header on every API request |
| Refresh token | 7 days | Used once to obtain a new access/refresh pair (rotation) |
Why 30 minutes for the access token? Users frequently switch away from the app for several minutes — e.g. to watch onboarding videos, use a messaging app, or check another tool. A shorter lifetime (e.g. 15 minutes) combined with a 7-minute proactive renewal buffer means a user who switches away for 5–10 minutes may return to a near-expired token. A single network hiccup during renewal would then cause a logout. 30 minutes provides a comfortable window without meaningfully weakening the security posture given the 7-day refresh token.
Token rotation is enabled (ROTATE_REFRESH_TOKENS = True) and old refresh tokens are blacklisted after use (BLACKLIST_AFTER_ROTATION = True), so a stolen refresh token cannot be used more than once.
Proactive Renewal
Rather than waiting for a 401 response, the app renews the access token before it expires. Three paths trigger renewal:
| Path | Trigger | Implemented in |
|---|---|---|
| Startup | App launch or access token change | useAuthManager — useEffect on accessToken |
| Periodic | Every 7 minutes while authenticated | useAuthManager — useInterval |
| Foreground resume | App returns to foreground | useAuthManager — AppState listener |
Each path calls verifySession(TOKEN_RENEWAL_TIMER) (7-minute buffer) to determine whether renewal is needed. If the current time is within 7 minutes of expiry — or past it — renewal is triggered.
A fourth path handles reactive renewal: if an API call returns 401 despite a seemingly valid token (clock skew, race condition), the Axios interceptor in HttpClient attempts a refresh before retrying the original request.
Retry Logic & Failure Handling
All renewal paths use a shared retry function with 3 attempts and exponential backoff (1 s → 3 s → 9 s).
Failures are classified into two categories before deciding whether to log out:
| Error type | Examples | Action |
|---|---|---|
| Transient | Network timeout, no connection, 5xx, 408, 429 | Retry up to 3 times; if all retries exhausted, keep session and report a warning to Sentry |
| Permanent | 401 or 403 from the refresh endpoint (refresh token expired or revoked) |
Logout immediately, no retries |
This means a flaky network connection during a periodic renewal or foreground resume will not log the user out — the session is preserved and the next renewal attempt will succeed once connectivity is restored.
Parallel Refresh Deduplication
When the app returns to foreground, SWR revalidates all cached queries simultaneously. If the access token is expired, this produces multiple concurrent 401 responses. Without deduplication, each 401 would trigger its own refresh attempt — only one can succeed (token rotation blacklists the old refresh token), and the others would fail and potentially cause a logout.
Two layers of deduplication prevent this:
-
refreshSessionPromise(module-level inHttpClient) — the first401to reach the interceptor creates a single in-flight refresh promise. All subsequent401s from the same burst await that same promise rather than starting their own. -
moize.promiseonrenewSession(15-second cache) — deduplicates calls across all paths (interceptor, periodic timer, foregroundAppStatelistener) at the store level. A refresh that completes via one path will be returned as-is to any other path that fires within 15 seconds.
Relevant Files
| File | Role |
|---|---|
src/stores/auth.ts |
Token state, renewSession (moized), revokeSession, useAuthManager hook |
src/services/HttpClient.ts |
Axios interceptor, reactive 401 refresh, refreshSessionPromise deduplication |
src/services/AuthService.ts |
refreshToken() — calls the Django SimpleJWT refresh endpoint |
src/common/StaleWhileRevalidate.tsx |
SWR global config — foreground token check before revalidation |
Base-API/base/settings.py |
SIMPLE_JWT — token lifetimes, rotation, blacklist settings |