Using OAuth 2.0 authentication in Android has become common practice. It has been described countless times far better than I could, so I will not try to do that. What’s the point of this article then, you may ask. Well, I have seen different implementations of the same concept on different projects each containing small changes, but when we found an issue we had to update all of them. So, what I will describe in this article is the implementation which we reuse at Halcyon Mobile across most of our projects as a library. Also I promise a reusable solution for you at the end of the article.
Introduction
How is the OAuth 2.0 authentication supposed to work in a typical Android application? Let’s break it down:
- The server has APIs which do not require a session, such as login and signup, here we need to send a hardcoded clientId parameter
- When the user logs in, the app receives a refresh and access token from the API
- We will store these somewhere locally
- We send the access token with each and every request
- When the access token expires, we will receive a 401 HTTP error from the server
- We use the refresh token to get new access and refresh token
- And so on…
OkHttp Interceptors
Since OkHttp provides tools to attach data to every request we will take advantage of them. These are called interceptors. Thus, we will create a ClientIdParameterInterceptor to add the clientId header for all our requests which don’t require session.
/** | |
* Simple interceptor which adds the given [clientId] as query parameter to the request. | |
*/ | |
class ClientIdParameterInterceptor(private val clientId: String) : Interceptor { | |
@Throws(IOException::class) | |
override fun intercept(chain: Interceptor.Chain): Response { | |
val original = chain.request() | |
val url = original.url().newBuilder() | |
.addQueryParameter("client_id", clientId) | |
.build() | |
return chain.proceed(original.newBuilder().url(url).build()) | |
} | |
} |
Then, we will write another interceptor which adds the access token to each request with a session.
/** | |
* Interceptor which adds the Authorization token to the header of the request. | |
*/ | |
internal class AuthenticationHeaderInterceptor(private val authenticationLocalStorage: AuthenticationLocalStorage) : Interceptor { | |
@Throws(IOException::class) | |
override fun intercept(chain: Interceptor.Chain): Response = | |
chain.proceed( | |
chain.request().newBuilder() | |
.header(AUTHORIZATION_KEY, "${authenticationLocalStorage.tokenType} ${authenticationLocalStorage.accessToken}") | |
.build() | |
) | |
companion object { | |
private const val AUTHORIZATION_KEY = "Authorization" | |
} | |
} |
Easy enough, so what’s next? We should react when we receive an HTTP 401 and re-request our tokens. How can we do that?
Authenticator
Retrofit already has a way to do this, by bringing Authenticator to the rescue. How should we implement the authenticator, though?
We will use the refresh token and start a request to refresh the tokens and update our failing requests. If the refresh-token request fails, we will return the original HTTP 401 error so we can show retry to the user. Since we are on a background thread, this can be done safely synchronously. This is how it looks:
/** | |
* Synchronized [okhttp3.Authenticator] which refreshes the token when the request response is 401 - Unauthorized. | |
* | |
* @param refreshTokenService is used to run the token-refreshing request returning a [SessionDataResponse] | |
* @param authenticationLocalStorage the persistent storage for the session [SessionDataResponse] | |
* @param sessionExpiredEventHandler a listener for session expiration. | |
*/ | |
internal class OAuth2Authenticator( | |
private val refreshTokenService: AuthenticationService, | |
private val authenticationLocalStorage: AuthenticationLocalStorage, | |
private val isSessionExpiredException: IsSessionExpiredException, | |
private val sessionExpiredEventHandler: SessionExpiredEventHandler | |
) : Authenticator { | |
override fun authenticate(route: Route?, response: Response): Request? { | |
try { | |
val refreshTokenResponse = refreshTokenService.refreshToken(authenticationLocalStorage.refreshToken).execute() | |
val sessionDataResponse: SessionDataResponse? = refreshTokenResponse.body() | |
if (refreshTokenResponse.isSuccessful && sessionDataResponse != null) { | |
authenticationLocalStorage.save(sessionDataResponse) | |
// retry request with the new tokens | |
return response.request() | |
.newBuilder() | |
.header(AUTHORIZATION_KEY, "${authenticationLocalStorage.tokenType} ${authenticationLocalStorage.accessToken}") | |
.build() | |
} else { | |
throw HttpException(refreshTokenResponse) | |
} | |
} catch (throwable: Throwable) { | |
when (throwable) { | |
is HttpException -> { | |
if (isSessionExpiredException(throwable)) { | |
onSessionExpiration() | |
return null // couldn't refresh show error to the user | |
} | |
} | |
} | |
} | |
// return the request with 401 error since the refresh token failed 3 times. | |
return null | |
} | |
/** | |
* On SessionExpiration we clear the data and report the event. | |
*/ | |
private fun onSessionExpiration() { | |
authenticationLocalStorage.clear() | |
sessionExpiredEventHandler.onSessionExpired() | |
} | |
} |
Tying it together
Okay, that’s clear so far. Next, we should tie this together and we are done, right? Almost… We want to use Retrofit for our refresh-service, which is used by the authenticator which is needed to create the Retrofit. Uh, that is a circular dependency, how do we go about it? Well, since we have two different set of request types we can use two different Retrofit instances for them to resolve this issue.
It is important to create the Retrofit instances from the same builder because this way they will share the same thread pool.
This is how our setup looks like:
val okHttpClient = OkHttpClient.Builder().build() | |
val retrofit = Retrofit.Builder().build() | |
val sessionlessOkHttpClient = okHttpClient.newBuilder().addInterceptor(ClientIdParameterInterceptor(clientId)).build() | |
val sessionOkHttpClient = okHttpClient.newBuilder().addInterceptor(AuthenticationHeaderInterceptor(authenticationLocalStorage)) | |
.authenticator( | |
OAuth2Authenticator( | |
refreshTokenService = refreshTokenRetrofitService, | |
authenticationLocalStorage = authenticationLocalStorage, | |
sessionExpiredEventHandler = sessionExpiredEventHandler, | |
isSessionExpiredException = isSessionExpiredException | |
) | |
) | |
.build() | |
val sessionRetrofit = sessionlessRetrofit.newBuilder().client(sessionOkHttpClient).build() |
The authenticationLocalStorage, sessionExpiredEventHandler, isSessionExpiredException, refreshTokenRetrofitService will be described later.
So far so good. If we try this out everything works and the token is refreshed. Now we are surely done, right? Yet again, almost. What happens if we have two requests in parallel and the token is expired? Both requests get HTTP 401 so the authenticator is called two times to refresh the token: one of them succeeds, the other fails. Uhh, that’s no good, so what can be done?
Well, the best would be not to send two refresh token requests, and the easiest way to achieve this is using synchronized. Yes, yes, synchronized has been demonized, but for this purpose it works well, since we need to wait for the new tokens for every request.
So, we move our refresh call into a synchronization block and check the authorization header just to make sure we do start a refresh token only if needed, and if it’s outdated we update the request and retry it.
Token Storage
Well, now we are surely done, yes we are done, but not done-done. We still have to store the tokens somewhere and the most obvious place is the shared preferences. So this is how that looks:
Describing the missing parts
IsSessionExpired is a class which just checks if the response is “the session no longer valid”.
SessionExpiredEventHandler is an interface. The implementation most probably will navigate the user to the landing page and show an error.
RefreshTokenRetrofitService is the Retrofit service to call the Refresh-Token API.
And now, if we make sure after login/signup that we save the tokens, we are finally done.
Reusable?
I promised you reusable code in the beginning, however, copy-pasting code is not really that. Instead, I give you this: we have already made this implementation with automatic parsing, default and configurable setup which also includes the storage. If you wish to use it or contribute, here it is the GitHub repo, where you can find the step by step setup. Take a peek, every feedback is very much appreciated. Thank you! Now go code ;)
Useful links
Gergely Hegedüs is an Android developer at Halcyon Mobile, a full-service mobile app design and development agency that creates award-winning mobile products for bold startups and brands.
Rookie developer here trying to learn about Oauth with Kotlin. I found your article really nice to learn a lot of this topic. Really appreciated