Implementing JWT Token Authorization in Mobile App
JWT — this is token format, not authorization protocol. Important to understand because many projects build own "JWT auth" without understanding what exactly they protect and from what.
Typical picture: backend generates JWT, signs HS256 (HMAC-SHA256), mobile app gets token, puts in UserDefaults and sends in Authorization: Bearer header. Works. But question is how secure.
Storage problem and where it bites
UserDefaults (iOS) and SharedPreferences (Android) — plaintext storage. On iOS without jailbreak hard to reach them, but iOS backup in iTunes/iCloud includes them. User backups device on computer with macOS — backup keys without password stored openly. Via idevicebackup2 and specialized utilities JWT can be extracted in minutes.
Simple rule: JWT stored only in Keychain (iOS) or Android Keystore/EncryptedSharedPreferences (Android). No UserDefaults, no AsyncStorage in React Native without additional encryption.
In React Native especially painful — AsyncStorage plaintext by default. Library react-native-keychain solves problem for iOS and Android, wrapping platform storages.
Client-side verification: what to check and what not
Mobile app can decode JWT and read claims — for displaying user name, checking roles in UI, determining expiration for proactive refresh. But verify signature on client meaningless if JWT signed with symmetric key (HS256) — client shouldn't know this key.
What client should check:
-
exp— token not expired (with small margin, ~30 seconds, for clock skew) -
iss— expected issuer -
aud— token intended for our app
If server uses RS256 or ES256 (asymmetric algorithm) — client can verify signature via public key. This makes sense for offline scenarios when no network but need check token locally.
Libraries: JWTDecode.swift (iOS), java-jwt from Auth0 or nimbus-jose-jwt (Android), jwt-decode (React Native).
Refresh and silent auth
Access token lives 15–60 minutes. Refresh token — days/weeks. Automatic update — HTTP client responsibility, not business logic.
On iOS with URLSession: custom URLSessionTaskDelegate or middleware pattern. In Alamofire — RequestInterceptor with methods adapt and retry. On Android with Retrofit — Authenticator (called on 401) or Interceptor (checks exp before request).
// Android Retrofit Authenticator
class TokenAuthenticator(private val tokenRepo: TokenRepository) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
if (response.code != 401) return null
val newToken = runBlocking { tokenRepo.refreshToken() } ?: return null
return response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
}
}
Protection from parallel refresh requests critical. If five requests simultaneously get 401 — five refresh attempts. Correct: Mutex (Kotlin)/NSLock (Swift) or async let with actor (Swift Concurrency). First thread does refresh, others wait result.
// iOS — actor for serialized refresh
actor TokenRefreshActor {
private var refreshTask: Task<String, Error>?
func refreshIfNeeded(using service: AuthService) async throws -> String {
if let task = refreshTask { return try await task.value }
let task = Task { try await service.refresh() }
refreshTask = task
defer { refreshTask = nil }
return try await task.value
}
}
Revocation and logout
JWT stateless by nature — cannot "revoke" token without additional infrastructure. Short exp + refresh token rotation — main protection. On logout:
- Delete tokens from Keychain/Keystore.
- Call
/auth/logouton server → server invalidates refresh token in database. - If server maintains blocklist — access token also invalidated immediately.
Points 2 and 3 — server work. But mobile side must call logout endpoint, even if user offline (queue via WorkManager/BackgroundTasks).
Timeframe
JWT auth from scratch: storage, interceptor for attach/refresh, logout, unit tests — 4–7 business days. If need integration with existing backend and claims format coordination — plus 2–3 days for sync with backend team.







