A quick fix: Migrating Google login from OpenID to OAuth2

A guide to migrating your web application (in a hurry) from OpenID to Google OAuth2.
Added by Tom Clegg about 2 years ago


Perhaps you have a web application that uses OpenID to authenticate Google users. That will stop working on April 20, 2015 so you need to switch to OAuth2! Or, perhaps you are making a new web application and you just want a Google login button working as quickly as possible.

(If you're not planning to make any Google login buttons, this post won't be very interesting.)

Google provides many pages of helpful documentation about implementing OAuth2 and migrating from OpenID. As a supplement to that -- in case you don’t want to wade through all the exciting possibilities of Google’s many and varied APIs, and you just want to stop worrying about April 20 -- here is the surprisingly short story about how to implement OAuth2 from (nearly) scratch. It should be easier to migrate to OAuth2 than it was to implement OpenID in the first place.

The objectives are simple here:
  1. Implement Google login with OAuth2, so your users can still log in after April 20.
  2. Recognize your existing OpenID users when they log in with OAuth2. You don’t want them to show up in a brand new empty account.
This post focuses on the specifics of what needs to work, rather than how to get any particular library or deployment strategy to work. For the sake of clarity, I’ll assume:
  • You use a language/libraries that can do what PHP can do, like make HTTPS requests and decode JSON.
  • You can decode JSON web tokens (JWT). Link to a PHP library is provided below. (Others: find it yourself.)
  • Your web application is installed in only one place, and your source tree is private. (This probably isn’t true, but I’m sure you can figure out the relevant config and deployment stuff yourself so I won’t discuss it here.)
There are four things you need to do.
  • Establish credentials for communication between your web application and Google.
  • Make a new login button for unauthenticated users to click.
  • Make a new callback handler. This sets up a cookie/session when Google assures you that user 12345 has clicked the login button.
  • Make account migration code in (or after) the callback handler, so you can recognize that user 12345 logging in with Google+ today is the same person as user https://google/accounts/o8/id?id=BlUrFl… who logged in with Google OpenID yesterday.

Step 0. Get ready.

Before getting to work, you need to make two easy decisions.
  • Your application’s realm. This is https://your-app.example.com/. It’s the same realm you used with OpenID. Usually, this is the root URI of your application.
  • Your OAuth2 callback URI. This is where you find out that a user is trying to log in to your app using Google. For our example we’ll use https://your-app.example.com/google-oauth2.php.

Step 1. Tell Google Developer Console how to recognize your web application.

  • Visit https://console.developers.google.com/project
  • Click “Create Project”. Give it a name and ID.
  • In the new project, click “APIs” in the “APIs & auth” section in the left nav.
  • Find “Google+ API” in the big list and enable it.
  • Click “Credentials” in the “APIs & auth” section in the left nav.
  • Under OAuth, click the “Create a new Client ID” button. In the dialog:
  • Application type: Web application
  • Authorized Javascript origins: https://your-app.example.com/
  • Authorized redirect URIs: https://your-app.example.com/google-oauth2.php
  • Yes, you can use stuff like “localhost:1234” in both of these fields, for testing.
  • Click “Create Client ID”
  • You should have a “Client ID for web application” table on the right.
  • Copy the client ID and client secret. You’ll need those soon.

Step 2. Make a new login button.

There are complicated fancy ways to do this with Google JS libraries, but you’re going to do it the easy way instead:

<form action="https://accounts.google.com/o/oauth2/auth" method="get">
<input type="hidden" name="response_type" value="code" />
<input type="hidden" name="client_id" value="client_id_from_dev_console_goes_here" />
<input type="hidden" name="redirect_uri" value="https://your-app.example.com/google-oauth2.php" />
<input type="hidden" name="state" value="see_note_about_state" />
<input type="hidden" name="scope" value="email openid profile" />
<input type="hidden" name="access_type" value="online" />
<input type="hidden" name="approval_prompt" value="auto" />
<input type="hidden" name="openid.realm" value="https://your-app.example.com/" />
<input type="submit" value="Log in using Google" />
</form>

Use your own values in the client_id, redirect_uri, and openid_realm inputs, of course.

Note about “state”: This just gets passed along to your google-oauth2.php. Maybe you want to put a timestamp here, so you can say “yay it took you 2.3 seconds to log in”. You can also leave it blank or delete the input entirely.

Step 3. Make a callback handler (google-oauth2.php).

This requires a JWT decoder. Here we use JWT.php from the BSD-licensed php-jwt project.

require_once('JWT.php');
$oauth2_code = $_GET['code'];
$discovery = json_decode(file_get_contents('https://accounts.google.com/.well-known/openid-configuration'));
$ctx = stream_context_create(array(
    'http' => array(
        'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
        'method'  => 'POST',
        'content' => http_build_query(array(
            'client_id' => 'client_id_from_dev_console_goes_here',                         // <-- edit this
            'client_secret' => 'client_secret_from_dev_console_goes_here',                 // <-- edit this
            'code' => $oauth2_code,
            'grant_type' => 'authorization_code',
            'redirect_uri' => 'https://your-app.example.com/login2.php',                   // <-- edit this
            'openid.realm' => 'https://your-app.example.com/',                             // <-- edit this
        )),
    ),
));
$resp = file_get_contents($discovery->token_endpoint, false, $ctx);
if (!$resp) {
    // $http_response_header here got magically populated by file_get_contents(), surprise
    error_log(json_encode($http_response_header));
    error_out('Error verifying token: ' . $http_response_header[0]);
}
$resp = json_decode($resp);
$access_token = $resp->access_token;
$id_token = $resp->id_token;

// Skip JWT verification: we got it directly from Google via https, nothing could go wrong.
$id_payload = JWT::decode($resp->id_token, null, false);
if (!$id_payload->sub) {
    error_log(json_encode($id_payload));
    error_out('No subscriber ID provided in ID token! See error log for details.');
}

// Hurray, authenticated.
//
// Edit the following section to suit your application.

$user_id = 'google+' . $id_payload->sub;
$user_email = $id_payload->email;

// Do whatever you do to keep people logged in. Maybe something like this.
session_start();
$_SESSION['user_id'] = $user_id;
That’s it for authentication.
  • $user_id is ‘google+X’ where X is a bunch of digits uniquely identifying this user. Unlike OpenID, X stays the same when the same user logs in to different web applications. You can make up your own translation from X to a local identifier, of course.
  • $user_email is the user’s email. $id_payload has other stuff too, all fascinating I’m sure.

Step 4. Migrate user accounts.

In 2017, Google will stop giving you the OpenID-to-Google+-ID mapping, so you’d better start weaning your system off the OpenID identifiers. Of course you can skip this step (and remove the bits mentioning “openid” above) if you’re working on a new web app with no existing OpenID accounts to worry about.

$openid_id = $id_payload->openid_id;
if ($openid_id) {
    // Anything in our database referring to $openid_id should change to refer to $user_id.
    search_and_replace($openid_id, $user_id);
}

Done!

(Try it once to make sure it works, if you’re into that sort of thing.)


Comments