Android – API Interceptor

API controllers exist in 99% of the applications worlwide. Most developers are very familiar with APIs and server mechanisms. What happens when it comes to auth session though?
The problem
Imagine you have a OAuth2 token (access & refresh tokens) retrieved from a login request. The access token expires every 1 hour and refresh token every 14 days. Now, let’s say that you are trying to perform an API request at the exact moment the access token expires!
The common way
Most developers would check here if the access token is valid. If the token is valid it will be used to complete the request. If not, the refresh token would be used to refresh the access token before proceeding to the request. But wait… This is not the way right? Exactly!
The solution
In this case we use the famous Authenticators. An authenticator is an interface implementation that is injected at your API client and performs all these checks before any request is performed. If the access token requires an update, the interceptor will perform that update. Let’s see an example:
The authenticator interface has 1 function, the authenticate:
fun authenticate(route: Route?, response: Response): Request?
Here, we can check whether the tokens are valid and return the request or null if we can not authenticate it. Notice that authenticate will only be called when a request requires authentication. Let’s see a full implementation:
override fun authenticate(route: Route?, response: Response): Request? {
logger.d("[${javaClass.name}] :: Request needs authentication. Starting process.")
synchronized(this) {
// Get tokens.
val accessToken = authController.getAccessToken()
val refreshToken = authController.getRefreshToken()
if (!accessToken.isNullOrEmpty() && response.request.header("Authorization") == null) {
logger.d("[${javaClass.name}] :: Access token is valid. Adding as header.")
return response.request.newBuilder()
.header("Authorization", accessToken)
.build()
} else if (!refreshToken.isNullOrEmpty()) {
logger.d("[${javaClass.name}] :: Access token is invalid. Refreshing token.")
return runBlocking {
// Try to refresh token.
when (interactor.refreshToken()) {
is AzureRefreshPartialState.Success -> {
response.request.newBuilder()
.header("Authorization", authController.getAccessToken() ?: "")
.build()
}
is AzureRefreshPartialState.Failed -> {
onCanNotAuthenticate(context, authController)
null
}
}
}
}
logger.e("[${javaClass.name}] :: Both access & refresh tokens are invalid.")
// Can not satisfy challenge.
onCanNotAuthenticate(context, authController)
}
return null
}
We can now build our OkHttp client using the following code and the authenticator will do it’s job:
return OkHttpClient.Builder()
.authenticator(apiAuthenticator)
.addInterceptor(apiInterceptor)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(CurlInterceptor {
logger.d("curl :: $it")
})
.readTimeout(API_TIMEOUT, TimeUnit.SECONDS)
.connectTimeout(API_TIMEOUT, TimeUnit.SECONDS)
.build()