OkHttp Authenticator – Selectively Reauthorizing Requests

Leveraging OkHttp3’s Authenticator API to more intelligently handle re-authorizations in a mixed-auth environment.

or:

The Trials and Tribulations of Mixed-Auth Under Retrofit2/OkHttp3

Occasionally, when in the process of cutting over from legacy authentication mechanisms to a more modern token-based authentication scheme such as OAuth, one can find themselves for a time having to simultaneously support both the tokens and the legacy endpoints.  Such was the case at a recent client of ours, wherein some of the endpoints were secured via a custom OAuth provider, but there lingered several legacy endpoints secured via basic authentication.

When it comes to network stacks, our most common approach on the Android side is to inject the API dependency via Dagger, most commonly implemented using Retrofit annotations with an RxJava call adapter.  Ideally, we have a single underlying OkHttp client, regardless of the number of Retrofit interfaces making use of it; this helps to keep the injection graph simple. Our challenge in this instance was to figure out how to support both flavors of authentication simultaneously and seamlessly, from the same injected client.

Historically, under Retrofit/OkHttp2, authorization injection into the request was done as part of an OkHttp request Interceptor attached to the OkHttp client.  The interceptor’s sole purpose is to grab a request and insert the authorization header prior before it makes its way out to the web, as evinced in the override:

override fun intercept(chain: Interceptor.Chain?): Response {
    val request = chain!!.request()
    val authenticatedRequest = request.newBuilder()
            .header("Authorization", credentials).build()
    return chain.proceed(authenticatedRequest)
}

While this approach is a little dated, it is a valid means of injecting authorization headers and has served us well ehough in the past.
Unfortunately for our mixed-auth case, Interceptors have no view into the genesis of the request and cannot make intelligent decisions about how to handle requests, without having to understand something of their content, tightly coupling the code to the request, which we wish to avoid.

Enter Retrofit2/OkHttp3 and the Authenticator API.

As previously stated, the mixed auth environment required by our client rendered the boilerplate Interceptor approach unworkable, and we had to rewrite it with two important requirements:

  1. The authorization header must be added independently of the the OkHttp client to keep the client decoupled from our external realities
  2. The OkHttp client must be able to differentiate between those requests that require token re-authorization and those that do not, and act accordingly

To accomplish the first, we quickly settled on Retrofit2’s @Header annotation, which allowed us to specify in the retrofit interface which authentication scheme to user for each individual endpoint:

// A legacy endpoint that is not wired up to Oauth yet:
@GET(BuildConfig.API_SUB_URL + "a_basic_auth_endpoint/")
fun getABasicAuthEndpoint(
        @Header("Authorization") basicCredentials: String =
                credentialsDelegate?.userBasicAuthCredentials() ?: ""
): Completable
 
// An endpoint that is secured with the user's login token
@GET(BuildConfig.OAUTH_SUB_URL + "an_oauth_endpoint/")
fun getAnOauthEndpoint(
        @Header("Authorization") bearerToken: String =
                credentialsDelegate?.userOauthBearerToken() ?: ""
): Completable

This change decoupled our OkHttp client from the authorization decision making process, allowing us to use a single client regardless of the fact that it had to serve two different authentication schemes.

The second requirement posed a significantly larger hurdle, but OkHttp’s Authenticator api provided us with the framework needed to accomplish the task.

Unpacking the Authenticator API

At it’s heart, an OkHttp Authenticator is an object attached to the OkHttp client whose sole purpose is to respond to 401 – Unauthorized errors and take appropriate action with the request.

The Authenticator interface consists of a single mandatory override:

abstract fun authenticate(route: Route?, response: Response?): Request?

As can be readily inferred, anytime an OkHttp3 client receives a 401 response from a server, it passes the route and the response to the authenticator, to ask for a new (replacement) request to run in its stead.  If that request is supplied in the return value, the client substitutes the request for the original  one, and provides its response to the calling code, rather than the 401 failure.

If null is returned, the request is not retried and the 401 failure bubbles back up to the calling code.

Bending Authenticator to Our Will

To meet the needs of our client, as well as satisfy our preferences for a single, injected client, our Authenticator will have to accomplish the following:

  1. Automatically detect 401 – Unauthorized errors
  2. Differentiate between basic auth and bearer auth requests, refreshing only the bearer auth 401s.
  3. Detect repeated retries to prevent infinite retry loops in the event of a server-side authorization malfunction.

The first one, which is by far the hardest to accomplish, we get for free with the Authenticator API.

The second is fairly trivial; the Authenticator simply checks to see if the original request had an “Authorization” header with the prefix “bearer: “.  If it does, we know that the request was submitted with a bearer token, and the 401 corresponds to a stale OAuth token.

private fun hasBearerAuthorizationToken(response: Response?): Boolean {
	response?.let { response ->
		val authorizationHeader = response.request().header("Authorization")
		return authorizationHeader.startsWith("bearer: ")
	}
	return false
}

So now we have the means to detect a 401 Unauthorized return code on a service call secured via OAuth.  Our processing for refreshing is fairly straight forward; we have a synchronous call to a delegate that returns the refreshed token, if possible, and we resubmit the request.

But we still have our third criteria to deal with – that of not devolving into an infinite retry loop in the event of our authentication and refresh services not playing nicely.

Our approach to the final issue is to simply use a custom http header to track our retry. Prior to re-authenticating the request, we check to see if our retry header exists, and what the count is. The count is screened against a threshold constant, and we return null if the threshold is exceeded. No muss, no fuss! The state is maintained within the request itself, with no need for convoluted state management within the app itself.

Putting it all together, our authenticator looks like this:

Where the delegate supplies all of the credentials, and takes the form of:

For those of you interested in an example project spun up with a fully injectable example API driven by the Authenticator pattern above, check out my MixedAuthExample available on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*