Authorization and retrying of web requests for OkHttp and Retrofit
Updated:
Are you trying to set up authorization for your OkHttp or Retrofit web requests but aren't sure of the best way to do it? Have you explored Interceptor
and Authenticator
but aren't sure which to use or how best to use them? We will explore these concepts within OkHttp, how to sign web requests and also how to retry them if they fail due to a failed authorization attempt.
When setting up networking in our apps, authorization is almost always required, as not many remote APIs are completely un-authenticated. OAuth is a common system to use, relying on access tokens to protect our endpoints and refresh tokens to obtain new access tokens once they have expired. The idea is that the access token is added as an Authorization HTTP header on requests to let the API know we have access to a particular resource.
OkHttp provides Interceptors
which can alter web requests before they are sent out and Authenticators
that allow us to re-sign and retry requests that have failed due to authorization. It is very common, particularly on Android, to use Retrofit for networking, which uses OkHttp internally and so the same techniques apply to Retrofit as well.
Let's have a look at how we set this up and how it all works!
Signing requests
We will start by setting up our AuthorizationInterceptor
that adds the Authorization
header to all web requests that are sent off to our remote API.
Our access tokens are provided by AuthorizationRepository
, which either provides a stored access token or obtains a new one if the stored one has already expired. How this process works depends on the authorization each particular API uses, the important part here is that AuthorizationInterceptor
has a function it can call to get an access token to sign the web request with.
Our Interceptor
implementation needs to provide an intercept
function that can extract the current Request
, transform it and then pass this new request back into the chain. The transformation we need to apply involves adding an Authorization header that signs the request with our access token. It is worth noting that the exact format of the header may change depending on what type of tokens each API uses, in our example Bearer tokens are used which are pretty common.
class AuthorizationInterceptor(
private val authorizationRepository: AuthorizationRepository
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val newRequest = chain.request().signedRequest()
return chain.proceed(newRequest)
}
private fun Request.signedRequest(): Request {
val accessToken = authorizationRepository.fetchFreshAccessToken()
return newBuilder()
.header("Authorization", "Bearer ${accessToken.rawToken}")
.build()
}
}
Registering our interceptor just involves adding it to the builder we are using to create our OkHttpClient
for either use directly or with Retrofit.
fun okHttpClient(authorizationInterceptor: AuthorizationInterceptor) =
OkHttpClient.Builder()
.addInterceptor(authorizationInterceptor)
.build()
All outgoing web requests will now be signed with our access token.
Retrying failed requests
It is possible that our requests may fail with 401 Unauthorized
due to an issue with the access token we provided. This will usually be due to the access token having expired or being revoked on the server-side. To handle this situation we can build an OkHttp Authenticator
, that allows us to catch this case, add a new token and then retry the request.
We will start by extracting an extension function to set the Authorization header on a request.
fun Request.signWithToken(accessToken: AccessToken) =
newBuilder()
.header("Authorization", "Bearer ${accessToken.rawToken}")
.build()
We will build in a limit on the number of retries to avoid an infinite loop if authorization for a particular endpoint fails entirely. We can determine how many times a request has been retried from the response.
private val Response.retryCount: Int
get() {
var currentResponse = priorResponse
var result = 0
while (currentResponse != null) {
result++
currentResponse = currentResponse.priorResponse
}
return result
}
The implementation of Authenticator
receives the response that failed with 401 Unauthorized
and has to optionally provide a new request to be triggered. If our request has already been retried twice we will simply allow it to fail to avoid an infinite loop. Signing the request is done in the same way as in our AuthorizationInterceptor
above.
class TokenRefreshAuthenticator(
private val authorizationRepository: AuthorizationRepository
) : Authenticator {
override fun authenticate(
route: Route?,
response: Response
): Request? = when {
response.retryCount > 2 -> null
else -> response.createSignedRequest()
}
private fun Response.createSignedRequest(): Request? = try {
val accessToken = authenticationRepository.fetchFreshAccessToken()
request.signWithToken(accessToken)
} catch (error: Throwable) {
Logger.error(error, "Failed to re-sign request")
null
}
}
Registering our authenticator involves adding it to the builder alongside our interceptor.
OkHttpClient.Builder()
.addInterceptor(authorizationInterceptor)
.authenticator(tokenRefreshAuthenticator)
.build()
Web requests that fail with authorization issues will now be re-signed and retried!
Refreshing expired tokens
In the above AuthorizationInterceptor
and TokenRefreshAuthenticator
we obtain an access token using fetchFreshAccessToken
. This function has been built to return the stored access token if it hasn't expired yet or to obtain a new one if it has.
An important note is that the function signatures of both Interceptor
and Authenticator
require the request to be created or transformed synchronously. The process of refreshing an access token is likely asynchronous due to requiring its own web request to an OAuth API. We therefore need to call this asynchronous token refresh process synchronously, exactly how will depend on how on it is implemented.
- Kotlin Coroutines can use the runBlocking function
- Retrofit Call has a synchronous execute function
- RxJava Single has a synchronous blockingGet operator
Multiple authorization types
In our implementation so far we have assumed that all requests need to be signed in the same way, however, in reality this likely won't be the case. For example, there will likely be requests that need to be performed before a user has signed in such as the account creation request or maybe fetching some form of configuration.
To incorporate this, we can use the OkHttp Tag API, which can also be used in our Retrofit calls. We will tag our web requests with an AuthType
enum that specifies which type of authorization to be used.
enum class AuthType {
ACCESS_TOKEN,
CLIENT_CREDENTIALS,
NONE;
companion object {
fun fromRequest(request: Request): AuthType =
request.tag(AuthType::class.java) ?: ACCESS_TOKEN
}
}
With OkHttp the tag can be specified in the request builder and with Retrofit it can be added using an annotation for a particular call.
interface CreateAccountRemoteApi {
@POST("user/identity")
suspend fun createAccount(
@Body request: CreateAccountRemoteDto,
@Tag authorization: AuthType = AuthType.CLIENT_CREDENTIALS
): AccountCreatedRemoteDto
}
The AuthType
can now be extracted and used within our AuthorizationInterceptor
and TokenRefreshAuthenticator
, to decide how to sign the request.
class AuthorizationInterceptor(
private val authorizationRepository: AuthorizationRepository
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val newRequest = chain.request().signedRequest()
return chain.proceed(newRequest)
}
private fun Request.signedRequest() = when (AuthType.fromRequest(this)) {
AuthType.ACCESS_TOKEN -> signWithFreshAccessToken()
AuthType.CLIENT_CREDENTIALS -> signWithClientCredentialsToken()
AuthType.NONE -> this
}
}
Using this approach we can easily control the exact type of authorization each individual endpoint requires, giving us a great level of flexibility.
Wrap up
Adding authorization to web requests for OkHttp and Retrofit can be done without too much complexity, however, without knowing which parts of the API to use it may not be immediately obvious. By using an Interceptor
we can avoid requests failing due to being unauthorized and then we can combine this with an Authenticator
to also handle the cases where it does unfortunately fail.
I hope the article was useful. If you have any feedback or questions please feel free to reach out.
Thanks for reading!
WRITTEN BY
Andrew Lord
A software developer and tech leader from the UK. Writing articles that focus on all aspects of Android and iOS development using Kotlin and Swift.
Want to read more?
Here are some other articles you may enjoy.