This post will be about refreshing expired tokens from Google, which is a topic that the various blog posts on the internets and StackOverflow couldn’t seem to answer.
Android development is hilarious at times. It’s really just Java development, but motherfuck it’s stupidly difficult at times for no good reason at all, such as terrible documentation. I’m working on an app that uses Google Login as the primary authentication method. Just to clarify to the readers for what I’m talking about, it is referring to the Google Login documentation in this url
https://developers.google.com/identity/sign-in/android/sign-in
The above is the documentation for implementing Google Sign In in an Android app. The above documentation actually does a good job of showing you how to implement Google Sign In in an Android app. The problem is that the tokens you get back from Google expires on an hourly basis, meaning that you as a developer have to refresh the tokens once they expire. Unfortunately, the documentation for implementing this seems to be non-existent all over the internet. Just to give you an example, a screenshot of the StackOverflow post that gave me “I think I want to go back to web development” moment

Well, after a week of struggling, I finally solved this issue and I will be giving you straight up no-bullshit answer so that others don’t have to struggle like me.
Just as a preface, the app I’m working on is using Retrofit 2 and RxJava 2. A lot of blog posts and StackOverflow answers to this problem you’ll find online have solutions using RxJava 1.x. This is where the problem occurs. In RxJava 1.x, any non-successful HTTP “response” like 401, 400, 500 (basically anything else than 200, 201, 202, 204, you get the point), was considered to be an “error”. Thus, those responses were handled in the “onError” block and could be “retried” with the “retryWhen” method. To give you a quick example, here’s a code snippet on using RxJava 1 and Retrofit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
SomeService.getApi(someUser.getToken()).getBooks() .compose(bindUntilEvent(ActivityEvent.DESTROY)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() { @Override public Observable<?> call(Observable<? extends Throwable> observable) { // Refrsh token here refreshToken(); return observable; } }) .subscribe(new Subscriber<Response<ArrayList<Book>>>() { @Override public void onCompleted() { Log.d(LOG_TAG, "onCompleted()"); } @Override public void onError(Throwable e) { Log.d(LOG_TAG, "onError(): " + e.getMessage()); } @Override public void onNext(Response<ArrayList<Book>> arrayListResponse) { Log.d(LOG_TAG, "onNext()"); if (arrayListResponse.isSuccessful()) { // Do stuff } else { // Do error stuff } } }); |
With RxJava 1.x, if the token had expired, you would have handled the response on the onError
block and then you would refresh your token in the retryWhen
section of your RxJava code.
In RxJava 2.x however, any normal HTTP responses are considered not to be errors, which if you think about it, is the correct way of handling it. Whether then HTTP response is 404, 500, or whatever, the server did what it was supposed to do, return a HTTP response. Thus, it technically isn’t an error. Therefore, to refresh the token in RxJava 2.x, you need to check for the proper http response in the onComplete
block and then rerun your RxJava code if your token had expired. For example, in our app, the server returns a 401
response if the token from Google had expired. So without further ado, here’s the code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
private void getBooksData() { SomeService.getApi(someUser.getToken()).getBooks() .compose(bindUntilEvent(ActivityEvent.DESTROY)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<Response<ArrayList<Pipeline>>>() { @Override public void onCompleted() { Log.d(LOG_TAG, "onCompleted()"); } @Override public void onError(Throwable e) { Log.d(LOG_TAG, "onError(): " + e.getMessage()); } @Override public void onNext(Response<ArrayList<Book>> arrayListResponse) { Log.d(LOG_TAG, "onNext()"); if (arrayListResponse.isSuccessful()) { // Do Stuff } else if (arrayListResponse.code() == 401) { App.getTokenRefresher().signInAgain(getContext()); TokenRefresher tokenRefresher = new TokenRefresher(); tokenRefresher.signInAgain(getContext()); getBooksData(); } else { // Do error stuff } } }); } |
As you can see, in the onNext
section of the code, I check if the response code is 401, and if it is 401, I refresh the token and call the method getBooksData()
again recursively. This will retry the API call after the token has refreshed. And to show you the TokenRefresher
class code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public class TokenRefresher { private GoogleApiClient mGoogleApiClient; private String LOG_TAG = TokenRefresher.class.getSimpleName(); public TokenRefresher() {} public void signInAgain(Context context) { Log.d(LOG_TAG, "THIS IS SIGNING IN AGAIN"); GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestScopes(new Scope("https://www.google.com/calendar/feeds/")) .requestScopes(new Scope("https://mail.google.com/")) .requestScopes(new Scope("https://www.google.com/m8/feeds/")) .requestScopes(new Scope("https://www.googleapis.com/auth/userinfo.profile")) .requestScopes(new Scope("https://www.googleapis.com/auth/userinfo.email")) .requestScopes(new Scope("https://www.googleapis.com/auth/drive")) .requestServerAuthCode(context.getResources().getString(R.string.server_client_id)) .requestIdToken(context.getResources().getString(R.string.server_client_id)) .requestEmail() .build(); mGoogleApiClient = new GoogleApiClient.Builder(context) .addOnConnectionFailedListener(connectionResult -> { Log.d(LOG_TAG, "onConnectionFailed = " + connectionResult); handleSignInResult(null); }) .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { @Override public void onConnected(Bundle bundle) { Log.d(LOG_TAG, "onConnected bundle = " + bundle); handleSignInResult(null); } @Override public void onConnectionSuspended(int i) { Log.d(LOG_TAG, "onConnectionSuspended i = " + i); handleSignInResult(null); }}) .addApi(Auth.GOOGLE_SIGN_IN_API, gso) .build(); try { Thread signInThread = new Thread(() -> { ConnectionResult connectionResult = mGoogleApiClient.blockingConnect(); if (connectionResult.isSuccess()) { GoogleSignInResult result = Auth.GoogleSignInApi.silentSignIn(mGoogleApiClient).await(); handleSignInResult(result); } }); signInThread.start(); } finally { mGoogleApiClient.disconnect(); } } private void handleSignInResult(GoogleSignInResult result) { Log.d(LOG_TAG, "handleSignInResult"); if (result != null && result.isSuccess()) { Log.d(LOG_TAG, "Yay success"); User accountUser = new User(result.getSignInAccount()); User user = User.mergeMeData(accountUser, App.getUserCacher().getCurrentUser()); App.getUserCacher().setAsDefault(user); } else { if (result != null) { Log.d(LOG_TAG, "Something went wrong :("); } } } } |
As you can see, all that I’m doing for is reimplementing silentLogin
feature of the Google Login Android SDK. This seems to refresh the expired token into a valid one.
I hope this post helps people who are banging their head against the wall trying to figure out how to refresh Google Login tokens.