4

I want to refresh my access token when it gets expired. I have implemented Authenticator as shown below:

@Singleton
class TokenAuthenticator(
    val authService: Lazy<AuthService>,
    private val sharedPreferences: SharedPreferences
) : Authenticator {


override fun authenticate(route: Route?, response: Response): Request? {
    return authRequestWithNewToken(response.request)
}

private fun authRequestWithNewToken(
    request: Request
): Request? {
    getFreshAccessToken()?.let { freshToken ->
        return getNewRequest(request, freshToken)
    }
    return null
}

private fun getFreshAccessToken(): String? {
    val refreshResponse = authService.get().refreshTokenTemp().execute()
    if (refreshResponse.isSuccessful) {
        val updatedAccessToken =
            refreshResponse.body()?.data?.accessToken ?: return null
        val updatedRefreshToken =
            refreshResponse.body()?.data?.refreshToken ?: return null
        updateToken(updatedAccessToken, updatedRefreshToken)
        Timber.tag("TOKEN")
            .d("Auth Updated token\n AccessToken: $updatedAccessToken \n RefreshToken: $updatedRefreshToken")
        return updatedAccessToken
    } else {
        return null
    }
}

private fun getNewRequest(request: Request, accessToken: String): Request {
    return request.newBuilder()
        .header(
            AppConstants.PARAMS.AUTH,
            AppConstants.PARAMS.BEARER + accessToken
        )
        .header("Accept", "application/json")
        .header("User-Agent", AppConstants.PARAMS.USER_AGENT)
        .build()
}

private fun updateToken(accessToken: String, refreshToken: String) {
    with(sharedPreferences.edit()) {
        putString(AppConstants.SHAREDPREFERENCE.ACCESS_TOKEN, accessToken)
        putString(AppConstants.SHAREDPREFERENCE.REFRESH_TOKEN, refreshToken).apply()
    }
    AppVariables.SessionData.accessToken = accessToken
    AppVariables.SessionData.refreshToken = refreshToken
    Timber.tag("TOKEN").d("Token Refreshed")
}
}

Even after using this, since there are parallel API calls happening, my app was still timing out and the user was taken to the Login screen.

I couldn't figure out what was wrong even after spending a lot of time.

I saw people trying @synchronised and I tried to implement that with no success.

Finally I saw this where he added a dispatcher like this:

    @Singleton
    @Provides
    fun provideOkhttpClient(tokenAuthenticator: TokenAuthenticator): OkHttpClient {
     //***********like this**********************//
        val dispatcher = Dispatcher()
        dispatcher.maxRequests = 1
     //******************************************//
        return OkHttpClient.Builder()
            .dispatcher(dispatcher)
            .connectTimeout(180, TimeUnit.SECONDS)
            .readTimeout(180, TimeUnit.SECONDS)
            .writeTimeout(180, TimeUnit.SECONDS)
            .authenticator(tokenAuthenticator)
            .addInterceptor(TokenInterceptor())
            .build()
    }

which seems to have fixed my issue. However, I don't know the implications of this. Is this recomended to use in production?

I can see the following in the documentation:

The maximum number of requests to execute concurrently. Above this requests queue in memory, waiting for the running calls to complete. If more than maxRequests requests are in flight when this is invoked, those requests will remain in flight.

Am I missing something that might bite me in the ass later??

Other references: link1 link2

hushed_voice
  • 3,161
  • 3
  • 34
  • 66
  • Your question whether this approach is recommended for production, answer is: it depends on the specific requirements and priorities of your app. If avoiding conflicts like the token refresh issue is critical, you might consider alternative strategies such as improving your token refresh mechanism, optimizing your network calls. – HassanUsman Aug 28 '23 at 11:21
  • How should I try to improve it?? I am using the Authenticator call which is specifically made for refreshing the authenticator. Not really sure how to improve it further. Please point me in the right direction @HassanUsman – hushed_voice Aug 28 '23 at 15:33

1 Answers1

0

The requirement is to synchronize refresh attempts, to ensure that all in-flight API requests from view models get the same token (rotated) refresh response. So one view model makes the request and all others wait on a worker thread.

One way to do that is to queue up continutations as in this code of mine. That enables you to write code like this, to send the HTTP request only once. In your case the OkHttp dispatcher seems to be doing the same job, so it feels on the right tracks.

override suspend fun refreshAccessToken(): String {
    return this.concurrencyHandler.execute(this::performRefreshTokenGrant)
}

To be sure of the right behaviour you should test the expiry event. First, I would trace the HTTP(S) traffic and verify that refresh requests are fired only once. Also test that multiple refresh requests can be run after each other. It is quite easy to simulate access token expiry, via a local development option that adds characters to the access token to make it act expired. You can run my code example to see the approach.

Gary Archer
  • 22,534
  • 2
  • 12
  • 24