Sapan Diwakar

Software developer

Follow me on Twitter Check out my code on GitHub View some of my designs on Dribbble Take a look at my Linked In profile

Refresh OAuth access token with Retrofit, RxJava

A very common use case when working with OAuth is to refresh the auth token. One way could be to do it periodically. A much simpler way, although, is to try refresh the auth token when you see a 401 response for an authenticated user. This is very straightforward to hook in to your API calls, especially if you are using RxJava. Retrofit makes it even simpler, but it really can be used even without Retrofit.

We will use an Extension to hook this function right into the Observable class. If you are still using Java, you can still use this by adding another argument to pass in your Observable to the function.

fun <T> rx.Observable<Response<T>>.retryWithAuthIfNeeded(context: Context): rx.Observable<Response<T>> {  
    // Retrofit unsuccessful response will also be passed to onNext. Filter auth failures to try to refresh token
    return flatMap { response ->
        if (response.isSuccess || response.code() != 401) {
            [email protected] Observable.just(response)
        }
        Observable.error<Response<T>>(Exception("Unauthorized"))
    }.retryWhen { attempts ->
        // Make 3 attempts to refresh auth token
        attempts.zipWith(Observable.range(1, 3), { n, i ->
            Timber.d(n as? Throwable, "Request failed. Will try to refresh auth token")
            i
        }).flatMap { i ->
            Observable.create<Token> { subscriber ->
                Timber.d("Trying to refresh auth token. Attempt %d", i)
                val realm = Realm.getDefaultInstance()
                RetrofitService.refreshAuthToken(Token.currentToken(realm)?.refreshToken)
                        .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                        .subscribe({ response ->
                            Timber.d("Refresh token response: ${response.isSuccess}")
                            if (!response.isSuccess) {
                                if (response.code() == 401) {
                                    MaterialDialog.Builder(context)
                                            .title("Error")
                                            .content("You have been logged out because someone using your account has deleted
                                                    this device from your account. Please log in again to continue")
                                            .positiveText("OK")
                                            .onAny { materialDialog, dialogAction ->
                                                User.logout()
                                            }
                                            .show()

                                    subscriber.onError(Exception("Unauthorized"))
                                    [email protected]
                                }
                                subscriber.onError(Exception("Error refreshing auth token"))
                            } else {
                                val token = response.body()
                                Token.saveInRealm(token)
                                subscriber.onNext(response.body())
                            }
                        }, { e ->
                            Timber.d("Refresh token failed")
                            subscriber.onError(e)
                        })
                realm.close()
            }
        }
    }
}

Let's try to break it into pieces. We have used retryWhen to allow us to retry the current observable if we detect an error. I had used Observable<Response<T>> for API return types to catch error response in onNext. If you use Observable as the return type for your calls, you can skip the first flatMap.

The next part zips the first Observable so that we don't retry to refresh the auth token if the refresh fails 3 times. If we haven't yet tried to refresh the token thrice, we try to refresh the auth token using the service which in turn notifies the subscriber of any further errors or success.

I use realm to store my auth tokens, but you can change it as per your persistence strategy.

Here's how the extension can be used on your authenticated calls which should automatically try to refresh the auth token.

RetrofitService.listProjects().retryWithAuthIfNeeded(context)  
    .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
    // …

[comment]: # If you are looking for an iOS equivalent using RxSwift and Moya, check this out.