DEV Community

Cover image for Authenticator in Retrofit Android
Mohit Rajput
Mohit Rajput

Posted on • Updated on

Authenticator in Retrofit Android

There is a general use-case in any application that the authentication token expires, and you need to refresh the internally. After upgrading the token, the API call should be executed again, and UI should be updated. Let's understand this scenario by an example:

  • You are building a social network application which has various functionalities, i.e. new feeds, friend requests list, send a friend request, see comments on a post etc.
  • After login, you provide an authentication token(i.e. JWT token) to the user. This token has an expiry time of 10 mins(which could be dynamic at server-side), and this should be passed in the header of each API call.
  • When you go to the friend requests list screen, you get 401 Unauthorized which means you need to refresh the token.
  • You not only need to update the token but also make the call to friend requests list API again and show the list in UI.

The naive way of handling this is, on each API call, make a check of 401 status code and call the refresh token API again. After calling the API, make the call to required API again. This code needs to be written everywhere(i.e. in each API call callback).

Does Android(more specific, Retrofit) have any way to handle this scenario using a standard code?
The answer is YES!!! We have a way of handling this scenario in Retrofit using Authenticator.

You can achieve this functionality using simple steps which are described below:

1. Method to fetch updated token

Declare a method in the retrofit interface to fetch the updated token from the server, as shown below:

interface UserApiService {
    companion object {
        private const val REQUEST_REFRESH_TOKEN = "/oauth/token"
    }

    @FormUrlEncoded
    @POST(REQUEST_REFRESH_TOKEN)
    fun getAuthenticationToken(@FieldMap params: HashMap<String, String>): retrofit2.Call<AuthTokenResponse>

    ...
}
Enter fullscreen mode Exit fullscreen mode

2. Create an Authenticator class

Implemented okhttp3.Authenticator interface and override authenticate() method as shown below:

class TokenAuthenticator : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        // This is a synchronous call
        val updatedToken = getUpdatedToken()
        return response.request().newBuilder()
                .header(ApiClient.HEADER_AUTHORIZATION, updatedToken)
                .build()
    }

    private fun getUpdatedToken(): String {
        val requestParams = HashMap<String, String>()
        ...

        val authTokenResponse = ApiClient.userApiService.getAuthenticationToken(requestParams).execute().body()!!

        val newToken = "${authTokenResponse.tokenType} ${authTokenResponse.accessToken}"
        SharedPreferenceUtils.saveString(Constants.PreferenceKeys.USER_ACCESS_TOKEN, newToken)
        return newToken
    }
}
Enter fullscreen mode Exit fullscreen mode

The authenticate() method is called when server returns 401 Unauthorized. For calling ApiClient.userApiService.getAuthenticationToken(), we're using execute() to make it a synchronous call.

3. Prepare OkHttp client

Now set this TokenAuthenticator in OkHttpClient as shown below:

 OkHttpClient.Builder()
                .connectTimeout(TIMEOUT_IN_SECONDS.toLong(), TimeUnit.SECONDS)
                .readTimeout(TIMEOUT_IN_SECONDS.toLong(), TimeUnit.SECONDS)
                .authenticator(TokenAuthenticator())`
                .addInterceptor(MyInterceptor())
Enter fullscreen mode Exit fullscreen mode

That is it!!!
Now, whenever you make an API call, i.e. friend requests list API call and get 401, the retrofit will call API to refresh the token and make the same request, i.e. friend requests list call again.
You do not need to handle anything on the view layer for this.

Thanks for reading this article. I hope you liked it and understood the solution easily. For any doubts or suggestions, feel free to comment.
If you're new to Kotlin, you can read my previous blog Features You Will Love About Kotlin which will give you the understanding of this beautiful language.

Top comments (11)

Collapse
 
mohitrajput987 profile image
Mohit Rajput

Many folks requested me to write a good implementation of TokenAuthenticator class i.e. how to refresh token.
Soon I will publish a new article on how I refresh tokens in authenticator.

Collapse
 
ashubuntu profile image
ashubuntu

if requestParams potentially consists of username and password, how do you provide that on the fly especially when the primary functionality of the app is to log the user out to the login activity when token is expired. By the way, this technique surely informs about 401 code.

Collapse
 
mohitrajput987 profile image
Mohit Rajput

Ideally we should have a refreshToken() function instead of login again.
But if you want to login again, then you can display a modal to user to enter username and password then resume this functionality as per given in blog.

Collapse
 
adriyoutomo profile image
Adri

nice article, i think i should use this in my next project 😁

Collapse
 
mohitrajput987 profile image
Mohit Rajput

Thanks Adri. Sure we should use this.

Collapse
 
arieftb profile image
Arief Turbagus (TB)

Can I use this solution for parallel request API?

Collapse
 
mohitrajput987 profile image
Mohit Rajput • Edited

Ofcourse. Make sure you manage authenticate() method accordingly.
E.g. make getUpdatedToken() call synchronize

Collapse
 
ricindigus profile image
RICARDO MORALES

Awesome, thank you very much!!!!!!!

i need this for my work.
You saved me.

Collapse
 
mohitrajput987 profile image
Mohit Rajput

Thanks @ricardo

Collapse
 
ayoub_anbara profile image
ayoub anbara 🇲🇦

If refresh token is expired also, and request refresh token return 401, how to handle this case?

Collapse
 
mohitrajput987 profile image
Mohit Rajput • Edited

Here is the complete code which I used in one project:

package one.projectname.sdk.network

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route

/**
 * Created by Mohit Rajput on 09/03/22.
 */
class TokenAuthenticator(
    private val tokenManager: TokenManager
) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request {
        synchronized(this) {
            val sessionData = if (isRefreshNeeded(response)) {
                runBlocking { getUpdatedSessionData() }
            } else {
                getExistingSessionData()
            }

            return response.request.newBuilder()
                .header(HeaderKeys.SESSION_ID, sessionData.sessionId)
                .header(HeaderKeys.REFRESH_ID, sessionData.refreshId)
                .build()
        }
    }

    private fun isRefreshNeeded(response: Response): Boolean {
        val oldSessionId = response.request.header(HeaderKeys.SESSION_ID)
        val oldRefreshId = response.request.header(HeaderKeys.REFRESH_ID)

        val updatedSessionId = tokenManager.getSessionId()
        val updatedRefreshId = tokenManager.getRefreshId()

        return (oldSessionId == updatedSessionId && oldRefreshId == updatedRefreshId)
    }

    private fun getExistingSessionData(): ApiResponse.SessionData {
        val updatedSessionId = tokenManager.getSessionId()
        val updatedRefreshId = tokenManager.getRefreshId()
        return ApiResponse.SessionData(
            sessionId = updatedSessionId,
            refreshId = updatedRefreshId
        )
    }

    private suspend fun getUpdatedSessionData(): ApiResponse.SessionData {
        val refreshTokenRequest =
            ApiResponse.RefreshSessionRequest(tokenManager.getRefreshId())
        return when (val result =
            getResult { userApiService().refreshSession(refreshTokenRequest) }) {
            is ApiResult.Success -> {
                val sessionData = result.data.data
                tokenManager.saveSessionId(sessionData.sessionId)
                tokenManager.saveRefreshId(sessionData.refreshId)
                delay(50)
                sessionData
            }
            is ApiResult.Error -> {
                MySdk.instance().mySdkListeners?.onSessionExpired()
                return ApiResponse.SessionData()
            }
        }
    }

    private class CustomNetworkStateChecker : NetworkStateChecker {
        override fun isNetworkAvailable() = true
    }

    private fun userApiService(): UserApiService {
        val retrofit = RetrofitHelper.provideRetrofit(
            RetrofitHelper.provideOkHttpClient(CustomNetworkStateChecker(), tokenManager)
        )
        return retrofit.create(UserApiService::class.java)
    }
}
Enter fullscreen mode Exit fullscreen mode