Using the one-time-code flow with Google+ Sign-in

OAuth v2 background

In this post, I’m going to assume that you understand a little bit about the traditional OAuth v2 flows for authorization. If you haven’t already checked it out, you can get acclimated hands-on through the OAuth2 playground.  The key thing you need to understand for this post are the following concepts:

  1. Your objective with OAuth flows is to make API calls and you need to get an access token to make these API calls. Therefore, all of the work you do with OAuth should conclude with you holding an access token.
  2. This token is a bearer token meaning anybody who bears the token (like a ring bearer at a wedding, not one of these) can make those calls on behalf of the user.
  3. Because the access token is a bearer token, it expires after a period of time to limit the damage that can be caused should it be lost or intercepted and must be refreshed from the authorization server.
  4. There are a number of ways to get the access token and these patterns are defined as client-side, server-side, and installed application – however for web, discussed here, the only relevant flows are server and client.
  5. The client is secured by its credentials (secret and/or ID) and these credentials are used to ensure the OAuth server does not issue access tokens to malicious parties.

The two traditional flows for web are client-side flow and server-side flow. In the client-side flow, the client ID is associated with the JavaScript caller and only authorized origins (e.g. yourdomain.com) will be issued the all-important access token for making API calls. In the server-side flow, your server has a client secret associated with its client ID that is used to secure retrieving access tokens from the OAuth v2 server.

The client-side flow is useful because it allows you to do all of the fancy client-side things that require authorization while the user is on your web site.  For example, if you wanted to render a real-time editor like the drive realtime quickstart sample for your user on your website using the new realtime Drive API – you would need to authorize the user and use this authorization client-side. The server-side flow is useful because it allows you to securely make API calls on behalf of the user while they are not connected to your site.  For example, what if you wanted to use the Drive upload API to backup a user’s blog data to the cloud every night at 11:59 pm or if you wanted to write Google+ app activities when a user reads an article on your website or checks in to a place, you could do this offline from your server.

That’s nice, so why one-time-code flow then?

When Google introduced the Google+ Sign-in button, it tried to solve the scenario where you want to perform BOTH client and server-side operations. Google quietly launched a number of impressive client-side APIs and widgets alongside the sign-in button, like interactive posts, that require the user to be authenticated client-side as well as APIs that really light up server-side like app activities. By enabling both client-side and server-side authorization, Google could enable developers to make web applications where both the rich client scenarios and offline server operations are possible.

The Google+ team strongly backed the new one-time-code flow because it enables the core functionality of the latest Google+ APIs. You can get started with one-time-code flow by trying one of the Google+ quickstarts available from the Google+ developers site.  The following list contains the existing flows:

How does the one-time-code flow work?

After understanding the benefits and optionally seeing Silvano walk you through it, it might be worth taking one more pass for understanding what is going on in the flow at a high level. Let’s take a look at the one-time-code flow step-by-step:

hybrid-flow

Here’s how it works:

  1. The client-side flow is triggered to the OAuth server, redirecting the user to a popup or tab and the user signs in to the authentication server.
  2. The authentication server issues the client an access token (just like the client-side flow) and a single-use authorization code (just like the server-side flow). Note The access token is secured by restricting the issue to the authorized JavaScript origin for your server and the authorization code requires the client secret and ID to be exchanged for credentials.
  3. The client POSTs the code to the server and the server then performs the code exchange using its client secret against the Google OAuth server.
  4. The OAuth server issues a refresh token to the backend server and the server then can perform offline operations by exchanging this token for an access token any time the current access token expires.

After this flow is completed, you have an authorized client and can make backend calls server-side,  opening up all sorts of new possibilities.

“Why can’t I just pass the access token between the client and server?” you might ask…  The reason for not doing this is that the access token is a bearer token so passing this token is like sending your password around and so it’s vulnerable to man-in-the-middle attacks should someone find a way to intercept it. Additionally, the access token is only valid for 3600 seconds (1 hour) so it’s not very useful for the backend server if the user isn’t coming back to your site every hour. By passing the single-use authorization code to your server, you add an additional security measure when authorizing your backend.

Security precautions when using one-time-code flows

Now is a great time to talk about a few security precautions that should be used when performing the one-time-code flow.

Let’s start with passing the single-use authorization code from the client to the server.  You might have noticed that I emphasized that this code is POSTed to the server as opposed to being passed in a GET request.  This is done because it’s more difficult for a user to determine what’s going on in client-server calls when the data is POSTed. If a malicious party intercepts the code and has your client secret / client ID, they can exchange it for a refresh token which would be very bad for your user.  Additionally, a best practice would be to do this data transfer over HTTPS to further protect the code payload. Because the authorization code is single use, if it is intercepted and the malicious party attempts to exchange it after you have exchanged it, you are still protected against the malicious party getting access they shouldn’t have.

The following code shows how the POST data is formed on the client for sending to the server over XML HTTP request (XHR):

    /**
     * Calls the server endpoint to connect the app for the user. The client
     * sends the one-time authorization code to the server and the server
     * exchanges the code for its own tokens to use for offline API access.
     * For more information, see:
     *   https://developers.google.com/+/web/signin/server-side-flow
     */
    connectServer: function(gplusId) {
      console.log(this.authResult.code);
      $.ajax({
        type: 'POST',
        url: window.location.href + '/connect?state={{ STATE }}&gplus_id=' +
            gplusId,
        contentType: 'application/octet-stream; charset=utf-8',
        success: function(result) {
          console.log(result);
          helper.people();
        },
        processData: false,
        data: this.authResult.code
      });
    },

Next, let’s look at something else that is happening in the samples: cross-site request forgery (CSRF) protection. This is done so that 3rd party sites can’t create accounts pragmatically on your site and use your quota. You may have noticed the state variable passed as a parameter in the connect method. When the server renders the client-side page, it sets this state variable, passed along with the POST for the authorization code, similar to how the state is set in OAuth flows for protecting against CSRF. The server has also set this state variable in the user’s session. When the request comes to the server, the server validates that the state variable passed in the connect XHR matches the one set in the user’s session in Ruby:

    # Make sure that the state we set on the client matches the state sent
    # in the request to protect against request forgery.
    if session[:state] == params[:state]

Another check that is performed is user validation.  You want to make sure that the user accessing the client is the same user that you are authorizing on the server.  To do this, the user identifier on the client is passed to the server in the connect method.  The following client-side shows how the user id is determined after the user successfully authorizes on the client:

    renderProfile: function() {
      var request = gapi.client.plus.people.get( {'userId' : 'me'} );
      request.execute( function(profile) {
          $('#profile').empty();
          if (profile.error) {
            $('#profile').append(profile.error);
            return;
          }
          helper.connectServer(profile.id);
          $('#profile').append(
              $('<p><img src=\"' + profile.image.url + '\"></p>'));
          $('#profile').append(
              $('<p>Hello ' + profile.displayName + '!<br />Tagline: ' +
              profile.tagline + '<br />About: ' + profile.aboutMe + '</p>'));
          if (profile.cover && profile.coverPhoto) {
            $('#profile').append(
                $('<p><img src=\"' + profile.cover.coverPhoto.url + '\"></p>'));
          }
        });
      $('#authOps').show('slow');
      $('#gConnect').hide();
    },

The profile.id is part of the people resource and uniquely determines the user on Google+.  What’s particularly convenient is that on the server-side the OAuth v2 API, tokeninfo can be used to identify the user id associated with the token returned from the code exchange. This call does not require authorization and does not have associated quota so that you are protected from a denial of service by clients spamming your server with requests that would be performed against the OAuth API. The following code shows how the user is validated on the server in Ruby:

      if tokeninfo['user_id'] != params[:gplus_id]
        halt 401, 'Token\'s user ID doesn\'t match given user ID.'
      end

A final check, one that validates the client ID who was issued the authorization token matches the current application’s client, is performed.  This is done to protect your application from performing operations for the wrong client. This happens in two ways, the first way that it is done is the error field returned from the tokeninfo call and the second way is a direct validation of the client ID for the token. The following code shows how these checks are performed in Ruby:

      # Verify the issued token matches the user and client.
      oauth2 = $client.discovered_api('oauth2','v2')
      tokeninfo = JSON.parse($client.execute(oauth2.tokeninfo,
          :access_token => $client.authorization.access_token,
          :id_token => $client.authorization.id_token).response.body)
      if tokeninfo['error']
        halt 401, tokeninfo['error']
      end
      if tokeninfo['issued_to'] != $credentials.client_id
        halt 401, 'Token\'s client ID does not match app\'s.'
      end

At the conclusion of these checks, your server is authorized and you can be pretty certain that the authorization token that you have been issued is intended for your server and was issued to a user who actually signed in to your site. You can now store the refresh token and can then construct your user credentials for server-side API calls and the JavaScript client will automatically authorize itself because the user has authorized the client associated with your client ID for client-side API calls. If you lose the server credential, you will need to re-authenticate the user, a topic for another post.

Additional resources

At this point, you should have a pretty good understanding of the one-time-code flow and are ready to go off and integrate it with your sites and services. If you encounter issues along the way, there are loads of great ways to get help and jump-start the process. The following resources are the ones I recommend for now: