Token Refresh & Session Management¶
Goal¶
Maintain a secure and seamless session using a two-token system:
- Access token: short-lived, used in
Authorization: Bearer ... - Refresh token: long-lived, used only to obtain new tokens
Canonical Code¶
- Refresh orchestration (thread-safe):
ecall/Core/Networking/TokenRefreshManager.swift - 401 handling + retry:
ecall/Core/Networking/APIClient.swift - Storage:
ecall/Core/Persistence/KeyStorage.swift - Endpoint paths:
ecall/Core/Networking/APIEndpoint.swift
Endpoint¶
- Refresh endpoint:
POST /app/api/refresh-token - Request body:
{ "refreshToken": "..." }
Thread Safety (Single Refresh In-Flight)¶
TokenRefreshManager uses a Swift actor (TokenRefreshState) to ensure:
- only one refresh call is in progress at a time (
isRefreshing) - concurrent refresh requests wait in a queue (
refreshQueue)
Behavior:
- First request sets
isRefreshing=trueand performs the refresh call. - Subsequent requests while refreshing are queued.
- When refresh completes, all queued continuations are resumed with the same result.
Refresh Flow¶
Trigger points¶
A) Reactive refresh (on 401)¶
In APIClient, when an authenticated request returns 401:
- Parse structured error code:
ErrAccessTokenExpired→ refreshErrRefreshTokenExpired→ logoutErrDeviceNotRegistered→ logout
If ErrAccessTokenExpired:
TokenRefreshManager.shared.refreshAccessToken()- On success → retry the original request once
- On failure → logout
See also: ./api-client.md.
B) Proactive refresh (app lifecycle)¶
The app refreshes tokens proactively when returning to foreground (implemented in RootView.swift in the current codebase).
Failure Handling¶
Missing refresh token¶
If no refresh token exists in Keychain:
- refresh fails with
.unauthorized - app logs out via
AppState.shared.logout(remotely: false)
Refresh request fails¶
If refresh fails (network/server/invalid token):
TokenRefreshManagerfinishes and releases queued waiters- caller (
APIClient) logs out if needed
Logout Conditions¶
The app forces logout when:
- server returns
401withErrRefreshTokenExpired - server returns
401withErrDeviceNotRegistered - refresh retry still fails with
401 - refresh token is missing locally
Related Docs¶
- API client 401 behavior:
./api-client.md - SSL pinning:
../security/ssl-pinning.md