Google API OAuth with PhoneGap's InAppBrowser

If your PhoneGap project requires access to one of Google’s APIs, the first challenge you’ll likely run into is how to handle the OAuth dance in a PhoneGap application.

Google’s OAuth documentation seems to indicate OAuth 2.0 for installed applications fits the bill for a mobile application.

The idea is to use an embedded web browser to show the OAuth consent page. If the user grants access, we can get the authorization code out of the embedded browser’s title property. We PhoneGap developers have InAppBrowser for embedded browsing, so this should be a piece of cake!

If you want to follow along, head on over to the API console and create a new project. Select API Access and then click the button to create a new client ID. In the dialog that pops up, select installed application, choose your platform, and fill in any other required information for your platform. Afterwards you’ll see your client ID, client secret, and redirect URIs.

Installed application client ID

Now that the application is registered with Google, go ahead and create a new PhoneGap project. I recommend the command-line interface, but use whatever you’re comfortable with. I tested this solution with PhoneGap 2.8, 2.9, and 3.0. If you are using PhoneGap 3.0 or higher, you will need to install the InAppBrowser plugin. I have heard from some readers that this solution may not work with earlier PhoneGap versions, so if you run into problems, check your PhoneGap version first.

In the examples that follow, I’ll be using jQuery since it is fairly ubiquitous and it keeps the code terse. With little effort you could make this work with your library of choice, or no library at all.

To keep things simple let’s start with a view that has a login button and a placeholder for a message:

<div id="login">
  <a>Sign In With Google!</a>
  <p></p>
</div>

When the application starts up, we need to wait for the deviceready event so we can interact with parts of the PhoneGap API like InAppBrowser. After deviceready fires, we can bind an event handler to show the OAuth consent page when the login button is tapped, and update the placeholder with the status of the OAuth dance after it completes. Here’s what that looks like in code:

var googleapi = {
    authorize: function(options) {
        var deferred = $.Deferred();
        deferred.reject({ error: 'Not Implemented' });
        return deferred.promise();
    }
};

$(document).on('deviceready', function() {
  var $loginButton = $('#login a');
  var $loginStatus = $('#login p');

  $loginButton.on('click', function() {
    googleapi.authorize({
      client_id: 'YOUR CLIENT ID HERE',
      client_secret: 'YOUR CLIENT SECRET HERE',
      redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
      scope: 'SPACE SEPARATED LIST OF SCOPES'
    }).done(function(data) {
      $loginStatus.html('Access Token: ' + data.access_token);
    }).fail(function(data) {
      $loginStatus.html(data.error);
    });
  });
});

If we run this and tap the button, we’ll see the Not Implemented message below the button. We just need to make that call to googleapi.authorize work! The first thing we need to do is show the consent page. Fortunately, using InAppBrowser in PhoneGap is the same as opening a popup from JavaScript using the window.open method. Let’s replace that deferred.reject call with some code to open the consent page:

var authUrl = 'https://accounts.google.com/o/oauth2/auth?' + $.param({
    client_id: options.client_id,
    redirect_uri: options.redirect_uri,
    response_type: 'code',
    scope: options.scope
});

var authWindow = window.open(authUrl, '_blank', 'location=no,toolbar=no');

What we’re doing here is building the URL for Google’s OAuth consent page and opening it in a new web view. Next we need to deal with retrieving the authorization code at the end of the consent process. That’s where things get interesting!

The recommendation is to retrieve the authorization code from the browser’s title. However, there are a couple of problems with that.

First, the consent window is pointing at a different origin, so according to the same origin policy, we are not allowed to access any details of its document, including the title. In PhoneGap the rules around the same origin policy are relaxed, like for AJAX requests, but I suspect we aren’t dealing with a fully specification conformant window object either. So the document title likely isn’t exposed in any way that would allow us to access it.

Second, even if we could access the document title, the authorization code isn’t set in the title until the very end of the OAuth flow, and we have no way of knowing when it is set. So we would have to poll for the change. Alternatively, we could use InAppBrowser’s executeScript method to insert some code to use window.opener.postMessage to notify us when we have the authorization code. However, again, we’re not dealing with a real popup here, so opener is not set, which rules out postMessage as a means of communicating between our two web views.

The key to this problem is the loadstart event that InAppBrowser fires. The loadstart event includes the URL that InAppBrowser is about to load whenever the InAppBrowser’s location changes. So we need a way to detect and parse the authorization code from the URL. Unfortunately, with the redirect URI we are using, the authorization code does not get set in the URL. However, there are two redirect URI options for installed applications. Let’s modify our code to try http://localhost as the redirect URI:

googleapi.authorize({
  client_id: 'YOUR CLIENT ID HERE',
  client_secret: 'YOUR CLIENT SECRET HERE',
  redirect_uri: 'http://localhost',
  scope: 'SPACE SEPARATED LIST OF SCOPES'
});

If we try the OAuth flow again with this redirect URI, we see that granting access redirects us to http://localhost?code=your_authorization_code. Denying access redirects us to http://localhost?error=access_denied. We probably don’t have an HTTP server running on our mobile device, so we will most likely end up staring at some sort of error page, but it doesn’t matter because we can detect the authorization code in the loadstart event handler and close the InAppBrowser before any errors are displayed.

Let’s handle loadstart and parse the authorization code after the line where we opened the consent page with InAppBrowser:

$(authWindow).on('loadstart', function(e) {
  var url = e.originalEvent.url;
  var code = /\?code=(.+)$/.exec(url);
  var error = /\?error=(.+)$/.exec(url);

  if (code || error) {
    authWindow.close();
  }

  //TODO - exchange code for access token...
});

Finally! We have an authorization code (or an error) and we can exchange it for an access token:

if (code) {
  $.post('https://accounts.google.com/o/oauth2/token', {
    code: code[1],
    client_id: options.client_id,
    client_secret: options.client_secret,
    redirect_uri: options.redirect_uri,
    grant_type: 'authorization_code'
  }).done(function(data) {
    deferred.resolve(data);
  }).fail(function(response) {
    deferred.reject(response.responseJSON);
  });
} else if (error) {
  deferred.reject({
    error: error[1]
  });
}

That’s it! Now we can use our access token to interact with one of the many Google APIs. We still need to think about access token expiration and revocation, but fortunately I’ve covered those topics in another article.

Finally, keep in mind that OAuth is a standard, so even though this article is written specifically for Google’s OAuth implementation, there’s a good chance it will work (perhaps with a few small changes) for most OAuth 2.0 providers that support a client-side flow. For example, one reader reported that she was able to adapt this code to work with Quizlet’s OAuth implementation.

The complete source code for this example is available on GitHub.

comments powered by Disqus