Android: Refreshing token proactively with OkHttp Interceptors
The purpose of this post is to explain how we handle the authentication flow in our authentication SDK. The next post will cover how to test it.
In our SDK, we are using Retrofit + Gson and our web services are behind an OAuth authentication system. We will make use of OkHttp interceptors in order to achieve our goals.
So what do we want to achieve?
- Start an authenticated server request
- Refresh token if needed
- Execute the call with a valid token
At this point, we had to choose between two options for dealing with auth via OkHttp:
- To make use of the default Authenticator provided by OkHttp ⚠️:
This approach triggers a default interceptor whenever a 401 response is received. This solution is a bit easier to implement, but it requires a failing response of a call we would like to avoid. - To write a custom authenticator using interceptors ✅:
This approach will verify the token for every call before its execution. This way we avoid unauthorised calls beforehand, and act faster in a token outdated case.
I will explain the code step by step, but you can find the whole code at the end of the article with the extension functions we are using.
Let’s do it 💪
Let’s talk about our interceptor, where all the magic is going to happen. An interceptor is just an interface that later on will be consumed by OkHttp and will “intercept” or trigger when a call is made.
The interceptor may contain the logic you want, in our scenario it will handle the token refresh. An empty Interceptor (representing the image above) looks like this:
internal class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {}
...
}
It gets a chain as a parameter that will allow us to execute calls inside our interceptor, and returns a response that will be executed by OkHttp. Our mission is to return an authorised response when possible.
The interceptor will behave as follows:
First of all, we will pass as parameters the needed dependencies in order to make it testable (just satisfying SOLID’s Dependency Inversion principle here). This will be our new interceptor constructor:
internal class AuthInterceptor(
private val repository: TokenRepository,
private val authUrl: String,
) : Interceptor { ...
}
- repository: this will be our class in charge of dealing with the token (get, set, delete)
- authUrl: this is the complete URL needed to refresh the token.
Initialising our variables
- Get the current saved token
- Store our current request, so we can trigger it afterwards (more of this later)
override fun intercept(chain: Interceptor.Chain): Response {
val currentToken = repository.getToken()
val originalRequest = chain.request()
The happy path
The first case we will cover is when we already have a valid token stored in our repository so we just have to execute the call with the token as a header:
return if (!currentToken.transformToLocalEntity().hasExpired()) {
chain.proceedDeletingTokenOnError(
chain.request().newBuilder()
.addHeaders(currentToken)
.build()
)
}
Token Refresh
If our token has expired, we need to refresh it:
else {
val refreshTokenRequest = originalRequest
.newBuilder()
.get()
.url(authUrl)
.addHeaders(currentToken)
.build()
val refreshResponse = chain.proceedDeletingTokenOnError(refreshTokenRequest)
...
Ok, this is what happened here:
In order to make a request to the refresh token endpoint, we need to change our original request (which we stored previously, because we will need it later) to fit the new requirements. In this case, we changed the url and the headers before it’s execution, and stored the response for the following.
Check token refresh response
Having the refreshResponse, we just need to check if it is successful to determine if we already have our desired token or not:
If its successful:
- Extract the new token from the response’s body with the help of Gson.
- Save the token for further requests.
- Trigger the original request adding the new token as header
Otherwise:
The function “Intercept” asks for a response, so we return the failing response and just accept we got defeated… 😑
if (refreshResponse.isSuccessful) {
val refreshedToken =
Gson().fromJson(
refreshResponse.body()?.string(),
TiendeoTokenRemoteEntity::class.java))
repository.saveTiendeoToken(refreshedToken)
val newCall = originalRequest.newBuilder().addHeaders(refreshedToken).build()
chain.proceedDeletingTokenOnError(newCall)
} else chain.proceedDeletingTokenOnError(chain.request())
The following file shows the complete AuthInterceptor. Note that we used two simple extension functions.
Adding the interceptor to our calls
The only thing you will have to do to use the brand new interceptor, is to add it to your retrofit builder, something like this:
val httpClient = OkHttpClient.Builder()
httpClient.addInterceptor(AuthInterceptor(context))Retrofit.Builder()
.baseUrl("Your URL here")
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
.build()
And that’s it! 🎊
I hope this post helped, if you liked it, give it some claps! 👏
The next post will cover how we unit-tested this using mockwebserver, don’t miss it if you’re curious! 😸