Making Token-Based Authentication in OkHttp More Efficient

Joseph James (JJ)
6 min readAug 31, 2024

--

Photo by Joseph James : The Realization ✨

I was in Berlin recently, playing around with shadow photography in black and white. It’s amazing how stripping away color can sharpen your focus on what’s essential — the contrast, the light, the textures. It’s a lot like solving problems in code: sometimes, you need to cut through the noise and focus on what really matters to find the most efficient solution.

If you’ve dealt with token-based authentication in OkHttp, you know it can get complicated quickly when multiple requests are happening simultaneously. It’s not just about refreshing the token — it’s about managing the potential chaos that arises when several threads hit an expired token at the same time. The result? Blocked threads, unnecessary retries, and an overworked server. But with a few smart adjustments, you can streamline this process and keep things running smoothly.

Understanding the Problem

Let’s break it down. In many applications, an access token is used to authenticate API requests. But tokens have a limited lifespan — they expire. When that happens, the client needs to fetch a new one. In an ideal scenario, one request notices the token is expired, refreshes it, and then the other requests use the new token without issue. Unfortunately, real-world applications are rarely this simple.

Here’s what often happens instead:

- Multiple Threads, Same Task: Imagine several API calls occurring at the same time, all hitting that expired token simultaneously. Without proper synchronization, they might all try to refresh the token simultaneously. This leads to a lot of unnecessary network traffic and can strain your server.

- Thread Blocking: A common way to handle this is to use synchronization, allowing only one thread to refresh the token while others wait. But this can slow everything down. While one thread is busy refreshing, others are stuck in line, which can seriously impact your app’s performance.

- Inefficient Retry Mechanisms: If each request retries the token refresh without coordination, it can lead to a flood of requests hitting your server. This isn’t just inefficient — it can actually make your app less reliable, especially if the server starts throttling or rejecting requests.

We need a solution that minimizes redundant operations, keeps threads from being unnecessarily blocked, and ensures the server isn’t overwhelmed by retries.

The Common Approach: Simple, But Not Ideal

A lot of developers start with a straightforward approach: whenever a request encounters an expired token, it tries to refresh it. This typically involves using the `synchronized` keyword to prevent multiple threads from attempting the refresh simultaneously. The idea is that while one thread refreshes the token, the others should wait. It’s simple and works — up to a point.

The Basic Token Authenticator with Retry Mechanism

Here’s a basic implementation that includes a retry mechanism:

import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route

class GeneralTokenAuthenticator : Authenticator {
@Volatile private var accessToken: String? = null
private val maxRetries = 3 // Limit the number of retries
override fun authenticate(route: Route?, response: Response): Request? {
val retryCount = response.request.header("Retry-Count")?.toIntOrNull() ?: 0
if (retryCount >= maxRetries) {
return null // Stop retrying after reaching the limit
}
synchronized(this) {
if (isTokenExpired()) {
accessToken = fetchNewAccessToken()
}
}
return response.request.newBuilder()
.header("Authorization", "Bearer $accessToken")
.header("Retry-Count", (retryCount + 1).toString())
.build()
}
private fun isTokenExpired(): Boolean {
return true // Placeholder logic
}
private fun fetchNewAccessToken(): String {
return "new_access_token" // Placeholder logic
}
}

Why This Approach Falls Short

- Blocking Threads: The `synchronized` block forces other threads to wait while one refreshes the token. This can create a bottleneck, especially under heavy load, causing your app to slow down or even become unresponsive.

- Redundant Refreshes: If multiple requests hit the expired token at the same time, they might all attempt to refresh it simultaneously. This results in redundant network traffic and unnecessary load on your server.

- Limited Retry Management: While adding a retry mechanism prevents infinite loops, it doesn’t address the inefficiency of multiple threads retrying the refresh simultaneously. Each thread can still end up holding resources longer than necessary, which can strain your system.

This approach can work for simple applications with low concurrency, but as your app scales and the number of concurrent requests grows, these issues become more pronounced.

A Smarter Approach: Using Mutex with Ownership

To tackle these issues, you can use Kotlin’s `Mutex` to control access to the token refresh process. With this approach, only one request refreshes the token while others wait their turn. By assigning a unique UUID to each request, you can track which request is responsible for holding the mutex and avoid redundant refreshes.

Additionally, we can handle scenarios where the application needs to initiate a logout after reaching the retry limit by injecting a function to control this behavior.

An Improved Token Authenticator

Here’s how you can enhance your implementation:

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route

class ImprovedTokenAuthenticator(
private val mutex: Mutex,
private val isLogoutStarted: () -> Boolean,
private val startLogout: () -> Unit
) : Authenticator {
@Volatile
private var accessToken: String? = null
private val maxRetries = 3 // Limit the number of retries
override fun authenticate(route: Route?, response: Response): Request? = runBlocking {
if (isLogoutStarted()) return@runBlocking null // If logout has started, don't attempt to refresh
val requestId = response.request.header("Request-ID") ?: return@runBlocking null
val retryCount = response.request.header("Retry-Count")?.toIntOrNull() ?: 0
if (retryCount >= maxRetries) {
startLogout() // Trigger the logout process
return@runBlocking null // Stop retrying after reaching the limit
}
return@runBlocking if (mutex.holdsLock(requestId)) {
handleTokenRefresh(response, retryCount)
} else {
mutex.lock(requestId)
handleTokenRefresh(response, retryCount)
}
}

private fun handleTokenRefresh(response: Response, retryCount: Int): Request? {
if (!isTokenExpired()) {
return response.request.newBuilder()
.header("Authorization", "Bearer $accessToken")
.build()
}
accessToken = getNewAccessToken()
return response.request.newBuilder()
.header("Authorization", "Bearer $accessToken")
.header("Retry-Count", (retryCount + 1).toString())
.build()
}

private fun isTokenExpired(): Boolean {
return true // Replace with real token expiration check
}

private fun getNewAccessToken(): String {
return "new_access_token" // Replace with real token fetch logic
}
}

Key Enhancements

- UUID Interceptor: This interceptor assigns a unique UUID to each request, allowing you to track which request owns the mutex and ensuring that only one refresh occurs at a time.

import okhttp3.Interceptor
import okhttp3.Response
import java.util.UUID

class UuidInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestWithId = chain.request().newBuilder()
.header("Request-ID", UUID.randomUUID().toString())
.build()
return chain.proceed(requestWithId)
}
}

- Unlocking Interceptor: This ensures that only the request holding the mutex can unlock it, preventing premature unlocking by other requests.

import okhttp3.Interceptor
import okhttp3.Response

class UnlockingInterceptor(private val mutex: Mutex) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {

val requestId = chain.request().header("Request-ID") ?: return chain.proceed(chain.request())
return try {
chain.proceed(chain.request())
} finally {
if (mutex.holdsLock(requestId)) {
mutex.unlock(requestId)
}
}
}
}

Putting It All Together

Here’s how you can set up your OkHttpClient using these components:

import kotlinx.coroutines.sync.Mutex
import okhttp3.OkHttpClient

fun createOkHttpClient(
isLogoutStarted: () -> Boolean,
startLogout: () -> Unit
): OkHttpClient {
val mutex = Mutex()
return OkHttpClient.Builder()
.addInterceptor(UuidInterceptor()) // Assigns a unique ID to each request
.authenticator(
ImprovedTokenAuthenticator(
mutex,
isLogoutStarted,
startLogout
)
) // Handles token refresh with mutex
.addInterceptor(UnlockingInterceptor(mutex)) // Ensures the mutex is properly unlocked
.build()
}

The worst-case scenario:

- Holding Threads Active: With the common approach, you could potentially hold up 5 threads for their retry attempts, leading to inefficiency. However, with the new approach, you manage retries more efficiently, reducing this to just two retries in total. Once the mutex is unlocked, the other

requests quickly complete without unnecessary delays.

Wrapping It Up

By using Kotlin’s `Mutex` along with UUID and unlocking interceptors, you can make your token-based authentication much more efficient. This approach prevents threads from blocking each other, reduces unnecessary token refresh attempts, and manages retries in a way that’s gentle on your server.

In a real-world scenario, this means your application will be more responsive under load, your server will thank you for not overwhelming it with redundant requests, and your users will experience faster, more reliable interactions with your app.

This setup is especially beneficial as your app scales, ensuring that it continues to perform well even as the number of concurrent users and requests increases. If you’re looking to make your OkHttp client more resilient and efficient, this approach is definitely worth implementing.

Happy coding ✨🚀

--

--

Joseph James (JJ)
Joseph James (JJ)

No responses yet