Handling Token Expiration in Retrofit: Automatic Token Refresh with OkHttp
A Complete Guide to Seamlessly Managing Token Expiry and Refresh in Android Apps

Introduction
When working with Retrofit in Android applications, handling token expiration is a crucial part of API security. If your app uses authentication tokens, those tokens may expire at any time, requiring a refresh before making further requests. In this article, we will explore how to seamlessly handle token expiration using OkHttp’s Authenticator to ensure a smooth user experience.
The Problem: Expiring Tokens
Imagine you have an API service with multiple endpoints, such as:
@GET("products")
suspend fun getProducts(): Response<List<Product>>
This works well initially, but what happens when the access token expires? The backend responds with 401 Unauthorized
, and the request fails. The expected behavior should be:
- Detect the
401 Unauthorized
response. - Automatically refresh the access token using a
refresh_token
. - Retry the original request with the new access token.
- If the refresh fails (e.g., refresh token expired), log out the user.
Manually handling this across multiple API calls can be tedious and error-prone. Instead, we can use OkHttp Interceptors and Authenticators to automate this process.
Step 1: Create an Interceptor to Add Authorization Header
We first need an interceptor that appends the Authorization header to every API request.
class AuthInterceptor(private val sharedPreferences: SharedPreferences) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val accessToken = sharedPreferences.getString("access_token", "") ?: ""
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $accessToken")
.build()
return chain.proceed(request)
}
}
How it Works:
- Retrieves the access token from
SharedPreferences
. - Appends it to the Authorization header.
- Proceeds with the modified request.
Step 2: Create an Authenticator to Refresh the Token
The Authenticator
class is triggered when an API call fails with a 401 Unauthorized
. It will:
- Call the token refresh API.
- Store the new access token.
- Retry the original request.
Here’s how you implement it:
class TokenAuthenticator(
private val apiService: ApiService,
private val sharedPreferences: SharedPreferences
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
synchronized(this) { // Prevent multiple refresh calls
val currentAccessToken = sharedPreferences.getString("access_token", null)
val refreshToken = sharedPreferences.getString("refresh_token", null) ?: return null
// If the access token changed since the first failed request, retry with new token
if (currentAccessToken != response.request.header("Authorization")?.removePrefix("Bearer ")) {
return response.request.newBuilder()
.header("Authorization", "Bearer $currentAccessToken")
.build()
}
// Fetch new tokens synchronously
val newTokensResponse = apiService.fetchNewTokens(refreshToken).execute()
if (!newTokensResponse.isSuccessful) {
return null // Refresh failed, trigger logout
}
val newTokens = newTokensResponse.body() ?: return null
// Save new tokens
sharedPreferences.edit()
.putString("access_token", newTokens.accessToken)
.putString("refresh_token", newTokens.refreshToken)
.apply()
// Retry the original request with new token
return response.request.newBuilder()
.header("Authorization", "Bearer ${newTokens.accessToken}")
.build()
}
}
}
How it Works:
- Gets the refresh token from
SharedPreferences
. - Calls the refresh API synchronously to get new tokens.
- Saves new tokens in
SharedPreferences
. - Retries the original request with the new access token.
- If the refresh fails, returns
null
, signaling that authentication is no longer valid.
Step 3: Integrate with OkHttp Client
Now, we need to configure OkHttp to use both AuthInterceptor
and TokenAuthenticator
.
fun provideOkHttpClient(apiService: ApiService, sharedPreferences: SharedPreferences): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(sharedPreferences))
.authenticator(TokenAuthenticator(apiService, sharedPreferences))
.build()
}
Explanation:
AuthInterceptor
ensures every request has an up-to-date token.TokenAuthenticator
automatically refreshes tokens when a401
is received.- Retrofit will use this OkHttp client to handle expired tokens automatically.
Step 4: Set Up Retrofit
Finally, initialize Retrofit with the custom OkHttpClient:
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://your.api.url")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Now, whenever you call:
api.getProducts()
If the token is expired, it will be refreshed automatically, and the API call will be retried without any manual intervention.
Step 5: Handling Edge Cases
While this setup works well, there are some important edge cases to handle:
1. Refresh Token Expired?
If the refresh token is also expired, you must log out the user:
if (!newTokensResponse.isSuccessful) {
logoutUser()
return null
}
2. Multiple Requests Fail Simultaneously?
If multiple API calls fail at the same time, the app should refresh the token only once. The synchronized(this)
block ensures that only one refresh request happens at a time.
3. Network Failure When Refreshing?
If the network fails during token refresh, the original API call will also fail. Consider implementing a retry mechanism with exponential backoff.
Conclusion
By using AuthInterceptor
and TokenAuthenticator
, we have achieved automatic token refresh in Retrofit. This approach ensures:
✅ Seamless token refresh without user intervention.
✅ Prevents multiple refresh calls by synchronizing the process.
✅ Automatically retries the failed request after refreshing the token.
✅ Handles refresh failures gracefully by logging out the user if needed.
Final Thought
This method enhances the user experience by preventing disruptions due to expired tokens. With this in place, users never see token expiration issues, and your app remains functional at all times. 🚀
Do you use another approach for token refreshing? Let me know in the comments below! 👇

Dobri Kostadinov
Android Consultant | Trainer
Email me | Follow me on LinkedIn | Follow me on Medium | Buy me a coffee