I just stumbled upon an issue where I started sending too parallel refresh token requests to the backend server I built, which caused concurrency issues where there is a race condition in which all of these parallel requests are requesting and updating different refresh tokens at the same time.
The only solution I came up with is to use a StateFlow, a Channel and an unscoped IO coroutine to observe the refresh state so that only the first refresh token request succeeds, and while it's refreshing, the other parallel requests are blocked observing until they get a signal from the first refresh token request to use the new token.
It works, but I'm new to Kotlin and its coroutine APIs aand it looks hacky, I can't help it but think there's definitenly a more sensisble way to approach this.
class MyAuthenticator @Inject constructor(
private val refreshTokenUseCase: RefreshTokenUseCase,
private val sharedPrefs: SharedPreferences
) : Authenticator {
private val isRefreshingToken = MutableStateFlow(false)
private val newRequest = Channel<Request>()
override fun authenticate(route: Route?, response: Response): Request? {
// logic to handle blocking parallel refresh token requests to wait for the first refresh token request to use it instead of useless api calls:
if (isRefreshingToken.value) {
CoroutineScope(Dispatchers.IO).launch {
isRefreshingToken.collect { isRefreshingToken ->
if (!isRefreshingToken) {
val newToken = sharedPrefs.getToken().orEmpty()
val req = response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
newRequest.send(req)
}
}
}
return runBlocking(Dispatchers.IO) {
newRequest.receive()
}
}
isRefreshingToken.value = true
// logic to handle refreshing the token
runBlocking(Dispatchers.IO) {
refreshTokenUseCase() // internally calls refresh token api then saves the token to shared prefs
}.let { result ->
isRefreshingToken.value = false
return if (result.isSuccess) {
val newToken = sharedPrefs.getToken().orEmpty()
response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
} else {
// logic to handle failure (logout, etc)
null
}
}
}
}
I searched all over stack overflow and while I've found many suggested solutions, none of them actually worked, half of which suggested using synchronization to force the parallel to start in an ordered manner, which still wastefully calls the API for a refresh token far too many times.