Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jwt authentication tokens refresh logic inside ApolloInterceptor #2461

Closed
Sserra90 opened this issue Jul 22, 2020 · 3 comments
Closed

Jwt authentication tokens refresh logic inside ApolloInterceptor #2461

Sserra90 opened this issue Jul 22, 2020 · 3 comments
Labels
⌛ Waiting for info More information is required ❓ Type: Question

Comments

@Sserra90
Copy link

Question.
Automatically refresh JWT tokens using ApolloInterceptor.

Hy guys i'm working on an application the needs to refresh authentication tokens under the hood when it receives a Unauthenticated error from GraphQL. I implemented this before for normal API using OkHttp interceptors and i ported some of the logic to ApolloInterceptor.

I tried to look at internal Apollo interceptors source code to see how they work and i managed to make a working solution. However because it's my first time using the apollo-android library i would like to ask for some feedback maybe i'm overlooking something.

class ApolloRefreshTokenInterceptor(
        private val tokenService: TokenService,
        private val tokenStorage: TokenStorage
) : ApolloInterceptor {

    @Volatile
    private var disposed = false
    @Volatile
    private var retryCount = 0

    override fun interceptAsync(request: ApolloInterceptor.InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: ApolloInterceptor.CallBack) {

        chain.proceedAsync(request, dispatcher, object : ApolloInterceptor.CallBack {

            override fun onFailure(e: ApolloException) {
                e.printStackTrace()
                callBack.onFailure(e)
            }

            override fun onResponse(response: ApolloInterceptor.InterceptorResponse) {

                if (disposed) return

                if (isUnauthenticated(response)) {

                    // Get current tokens before refreshing.
                    val currentTokens = tokenStorage.get()

                    Log.d("GRAPHQL", "User unauthenticated")
                    synchronized(this) {

                        if (retryCount >= MAX_RETRY_COUNT) {
                            Log.d("GRAPHQL", "Retry count reached limit.")
                            callBack.onResponse(response)
                            callBack.onCompleted()
                            retryCount = 0
                            return
                        }

                        retryCount++

                        // Check if the token was not already refreshed.
                        if (wasTokenAlreadyRefreshed(currentTokens)) {
                            Log.d("GRAPHQL", "Token already refreshed.")
                            return chain.proceedAsync(
                                    request
                                            .toBuilder()
                                            .fetchFromCache(false)
                                            .build(),
                                    dispatcher,
                                    this
                            )
                        }

                        // Refresh token sync.
                        val result = runBlocking {
                            Log.d("GRAPHQL", "Refreshing token.")
                            refreshToken()
                        }

                        if (result.isSuccess()) {

                            Log.d("GRAPHQL", "Tokens refreshed")
                            // Remake request.
                            chain.proceedAsync(
                                    request
                                            .toBuilder()
                                            .fetchFromCache(false)
                                            .build(),
                                    dispatcher,
                                    this
                            )
                            return

                        } else {

                            Log.d("GRAPHQL", "Raise event cannot refresh token")

                            // Raise UserUnauthenticatedEvent to be handled by upper layer.
                            // We cannot refresh the session automatically, user needs to login again.

                            raiseUnAuthenticatedUserEvent()
                            callBack.onResponse(response)
                            callBack.onCompleted()
                            return
                        }

                    }

                } else {
                    retryCount = 0
                    callBack.onResponse(response)
                    callBack.onCompleted()
                }

            }

            override fun onFetch(sourceType: ApolloInterceptor.FetchSourceType?) {
                callBack.onFetch(sourceType)
            }

            override fun onCompleted() {
            }

        })

    }

    override fun dispose() {
        disposed = true
    }

    private fun isUnauthenticated(response: ApolloInterceptor.InterceptorResponse): Boolean {
        return if (response.parsedResponse.isPresent) {
            @Suppress("UNCHECKED_CAST")
            val errors = response.parsedResponse.get().errors as List<Error>?
            errors?.find { it.message == "authentication required" } != null
        } else false
    }

    private suspend fun refreshToken(): Result<Boolean> = tokenService.refresh()

    /**
     * Check if authentication tokens are different. If they are different it means another
     * thread already refreshed the access tokens.
     */
    private fun wasTokenAlreadyRefreshed(authTokens: TokenStorage.AuthTokens?): Boolean =
            authTokens != tokenStorage.get()

    private fun raiseUnAuthenticatedUserEvent() {
        // TODO raise event
    }


}

These are the pre-requisites that i took into account.

  • Make sure the method you use to manipulate the token is synchronized. Only one thread tries to refresh the token.
  • Count the number of retries to prevent excessive numbers of refresh token calls.
  • Make sure the API calls to get a fresh token not asynchronous.
  • Check if the access token is refreshed by another thread already to avoid requesting a new access token.

Is this the correct way or doing this kind of operation or is there any alternative ?

Thanks

@martinbonnin
Copy link
Contributor

@Sserra90 using an ApolloInterceptor is definitely one way to do it. Another option could be to use OkHttp interceptors. Both can work as long as you make sure to synchronise token accesses like you mentioned. Let us know if you have additional questions.

@martinbonnin martinbonnin added the ⌛ Waiting for info More information is required label Aug 6, 2020
@martinbonnin
Copy link
Contributor

Closing due to inactivity. @Sserra90 , let me know if you want it to be reopened

@MuhammadBahaa
Copy link

MuhammadBahaa commented Sep 16, 2021

Good Job!

I think you have to put refresh token logic's in onFailure() fun since if the API call returns with status code 401 (unauthorized) there is an exception that will be thrown and onResponse() will not continue then call onFailure() will be called, you depend always on the API call will return with code 200, and in some cases, that does not happen. so because of the exception which thrown, you have to handle onFailure() in case status code 401.

It would be like this

       override fun onFailure(e: ApolloException) {
                if (e.message?.contains("401") == true) {
                    if (disposed) return

                    Log.e("GRAPHQL", "User unauthenticated")
                    synchronized(this) {
                        val result = runBlocking(Dispatchers.IO) {
                            Log.e("GRAPHQL", "Refreshing token.")
                            refreshToken()
                        }

                        if (result) {
                            Log.e("GRAPHQL", "Tokens refreshed")
                            // Remake request.
                            chain.proceedAsync(
                                request
                                    .toBuilder()
                                    .fetchFromCache(false)
                                    .build(),
                                dispatcher,
                                this
                            )
                            return

                        } else {

                            Log.d("GRAPHQL", "Raise event cannot refresh token")

                            // Raise UserUnauthenticatedEvent to be handled by upper layer.
                            // We cannot refresh the session automatically, user needs to login again.
                            e.printStackTrace()
                            raiseUnAuthenticatedUserEvent()
                            callBack.onFailure(e)
                            return
                        }
                    }
                }
            }
            

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⌛ Waiting for info More information is required ❓ Type: Question
Projects
None yet
Development

No branches or pull requests

3 participants