Blog Infos
Author
Published
Topics
, , , ,
Published
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:

  1. Detect the 401 Unauthorized response.
  2. Automatically refresh the access token using a refresh_token.
  3. Retry the original request with the new access token.
  4. 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:
  1. Gets the refresh token from SharedPreferences.
  2. Calls the refresh API synchronously to get new tokens.
  3. Saves new tokens in SharedPreferences.
  4. Retries the original request with the new access token.
  5. 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 a 401 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
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

No results found.

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

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu