Implementing JWT Authentication in Mobile Apps
A recent case: a client stored JWT in SharedPreferences—after restoring from a backup, an attacker gained access to the API. We migrated storage to Android Keystore, and incidents stopped. According to OWASP, improper token storage is among the top-10 mobile app vulnerabilities. Implementing Keychain reduces leakage risk by 90% compared to UserDefaults.
We have been implementing JWT authentication for over 5 years and have done it in 20+ projects on iOS, Android, and hybrid platforms. Our approach eliminates storage leaks and race conditions during token refresh. Here's how it's done.
Why UserDefaults Is a Bad Choice for JWT?
UserDefaults (iOS) and SharedPreferences (Android) are plaintext stores. On iOS, iTunes/iCloud backups include this data. With idevicebackup2 and specialized utilities, JWT can be extracted in minutes. In 90% of cases, token leakage occurs precisely from such storage. Keychain is 100 times more secure thanks to hardware encryption—it protects data even with physical device access.
| Storage | Security | Backup Included | Recommendation |
|---|---|---|---|
| UserDefaults / SharedPreferences | Low | Yes | Do not use |
| Keychain (iOS) | High (hardware encryption) | Only with user permission | Use |
| EncryptedSharedPreferences (Android) | Medium (key in Keystore) | Depends on implementation | Use |
| AsyncStorage (RN) | Low | Yes | Use with react-native-keychain |
What to Check on the Client?
The mobile app can decode JWT and read claims—to display user name, check roles, determine expiration time. Verifying the signature on the client is pointless with HS256, since the secret key must not be known to the client. Mandatory checks:
-
exp—token not expired (with ~30 seconds buffer for clock skew) -
iss—expected issuer -
aud—token intended for our app
For asymmetric algorithms RS256/ES256, the client can verify the signature via the public key—this is useful for offline scenarios. Libraries: JWTDecode.swift (iOS), java-jwt from Auth0 (Android), jwt-decode (React Native).
How to Organize Automatic Token Refresh?
Access token lives 15–60 minutes, refresh token days/weeks. Automatic refresh is a task for the HTTP client. On iOS with URLSession, use a custom URLSessionTaskDelegate or middleware pattern; in Alamofire—RequestInterceptor. 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 against concurrent refresh requests is important. If five requests simultaneously get 401—five refresh attempts. The correct approach: Mutex (Kotlin) / NSLock (Swift) or async let with actor (Swift Concurrency). The first thread performs refresh, others wait for the 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 } } How to Handle Logout and Revoke?
JWT is inherently stateless—a token cannot be "revoked" without additional infrastructure. Short exp + refresh token rotation is the main protection. On logout:
- Delete tokens from Keychain/Keystore.
- Call
/auth/logouton the server—the server invalidates the refresh token in the database. - If the server maintains a blocklist—the access token is also invalidated immediately.
Steps 2 and 3 are server-side work. The mobile side must call the logout endpoint, even if the user is offline (queue via WorkManager / BackgroundTasks).
Comparison of HS256 vs RS256 Algorithms
| Property | HS256 | RS256 |
|---|---|---|
| Key type | Symmetric (single secret) | Asymmetric (public/private) |
| Client verification | Impossible (key not disclosed) | Possible via public key |
| Performance | High | Lower (~2x slower) |
| Recommendation | For internal microservices | For mobile clients |
What's Included in the Work
We provide:
- Secure token storage (Keychain/Keystore)
- Interceptor for automatic refresh with race condition protection
- Logout integration with server-side invalidation
- Unit tests for key scenarios
- Documentation for the team
- Transfer of storage access and signed builds
- Training for the client's team
- Support for one month after implementation
Work Process for JWT Authentication
- Analysis of existing scheme and tokens
- Design of secure storage (Keychain/Keystore)
- Implementation of interceptor for automatic refresh (with race condition protection)
- Integration of logout with server-side invalidation
- Unit tests for key scenarios
- Documentation for the team
- Transfer of storage access and signed builds
Timeline: JWT auth from scratch—4–7 working days. If integration with an existing backend is needed—add 2–3 days for synchronization. Clients who have implemented our scheme save an average of 40% of the budget on vulnerability fixes.
We guarantee your tokens will not be exposed, and the user experience will remain smooth. Contact us for an audit of your authorization system—we'll find vulnerabilities and propose solutions. Order a secure JWT scheme implementation—get results in 4–7 days.







