OAuth with PhoneGap's InAppBrowser: Expiration and Revocation

A few weeks ago I wrote about how to implement an OAuth login with PhoneGap's InAppBrowser. Well, it seems like a lot of people are looking for information about PhoneGap and OAuth, so I figured I’d write a follow up article.

At the end of the last article we had a way to launch the consent page in PhoneGap’s InAppBrowser, retrieve the authorization code, and exchange it for an access token. That’s great, but for a release-quality application we need to take things a bit further.

There are two more conditions we need to consider: access tokens can expire and access tokens can be revoked.

Refreshing Expired Access Tokens

Access tokens are valid for a finite period of time, then they expire. This means that after we receive an access token we can cache it and continue to use it until it expires. When an access token expires, we need to go get a new one.

An OAuth 2.0 provider will typically issue an access token with a refresh token. We can use the refresh token to retrieve a new access token without showing the consent page again.

At first the solution seems simple. Use setTimeout to refresh the token a few seconds before it expires. However, this could lead to hard-to-debug race conditions if the token refresh takes a long time to complete, but that’s not the worst problem.

Our PhoneGap app is running on a mobile device. What if we do not have a network connection when the refresh timer fires? What if the app is suspended in the background when the refresh timer fires? We could subscribe to PhoneGap’s online or resume events and refresh the token or reset the timer in each of those event handlers… but that will quickly become a mess.

The best way I’ve found to handle this is to check the status of the access token before each API request is sent. If the token is still valid, we’ll just pass it on to the original request. If the token is expired, we’ll refresh the token, cache the new token, and then pass it on to the original request.

Building on the OAuth example from the last article, we’re going to add two new methods. First, we need a setToken method to cache some information about the token:

googleapi.setToken = function(data) {
  localStorage.access_token = data.access_token;
  localStorage.refresh_token = data.refresh_token || localStorage.refresh_token;

  //Calculate exactly when the token will expire, then subtract
  //one minute to give ourselves a small buffer.
  var now = new Date().getTime();
  var expiresAt = now + parseInt(data.expires_in, 10) * 1000 - 60000;
  localStorage.expires_at = expiresAt;
};

I’m storing the token information in localStorage in this example, but you could also store it in a cookie, or in a file by using PhoneGap’s file system API if you want. Anywhere we retrieve an access token we’ll need to call this method to update the cache.

Next we need a getToken method that will transparently validate a cached access token or update an expired access token with a refresh token.

googleapi.getToken = function(options) {
  var deferred = $.Deferred();
  var now = new Date().getTime();

  if (now < localStorage.expires_at) {
    //The token is still valid, so immediately return it from the cache
    deferred.resolve({
      access_token: localStorage.access_token
    });
  } else if (localStorage.refresh_token) {
    //The token is expired, but we can get a new one with a refresh token
    $.post('https://accounts.google.com/o/oauth2/token', {
      refresh_token: localStorage.refresh_token,
      client_id: options.client_id,
      client_secret: options.client_secret,
      grant_type: 'refresh_token'
    }).done(function(data) {
      googleapi.setToken(data); //Remember I said we need to call this to cache the token?
      deferred.resolve(data);
    }).fail(function(response) {
      deferred.reject(response.responseJSON);
    });
  } else {
    //We do not have any cached token information yet
    deferred.reject();
  }

  return deferred.promise();
};

Here is how we can use the getToken method to ensure we have a valid access token before making an API request:

googleapi.getToken({
  client_id: 'YOUR_CLIENT_ID',
  client_secret: 'YOUR_CLIENT_SECRET'
}).then(function(data) {
  //Call an API method and return a new promise
  return googleapi.userInfo({ access_token: data.access_token });
}).done(function(user) {
  alert('Hello ' + user.name + '!');
});

Naturally, if the application involves many different API calls that all require access tokens, we’ll want to figure out some way to do this in one place. How to accomplish that will depend on the specific application, so I’ll leave it as an exercise for you!

Revoked Access Tokens

Now we’re handling expired tokens, but there is one more problem. Tokens can be revoked, and not necessarily only from within your app. Most OAuth providers have some sort of console where you can manage all of your OAuth grants. One of your app’s users might go to this screen and clean things up, and now that access token you cached doesn’t work anymore. Even if it was not yet expired!

If this happens, the only way to get a new access token is to ask the user for one by showing the consent page again.

The best way to handle this is to add an error handler for API requests that fail authorization. If authorization fails, there is likely some problem with the access token we used, so we need to throw it out and ask for a new one. Fortunately, we can accomplish this easily:

googleapi.getToken({
  client_id: 'YOUR_CLIENT_ID',
  client_secret: 'YOUR_CLIENT_SECRET'
}).then(function(data) {
  //Call an API method and return a new promise
  return googleapi.userInfo({ access_token: data.access_token });
}).done(function(user) {
  alert('Hello ' + user.name + '!');
}).fail(function() {
  //The token was revoked, prompt the user to login again
  app.showLoginView();
});

Just like in the last post, the full source code for this example is available on github. If you want to try out what happens when you revoke an access token, head to your Google Accounts page, click on the manage security link, then click on the review permissions link under connected applications and sites to manage your active OAuth grants.

comments powered by Disqus