Thursday, 6 March 2014

Migrating Away From Userinfo

As part of the move to full OpenID connect support recently, the "userinfo" scopes and endpoint were deprecated and scheduled for shutdown in September 2014. If you are using the userinfo API endpoint to retrieve email address or profile information for a Google user, now is the time to change! Luckily, it's just a few minutes of work to move from the userinfo API to the people.get API for most people, and wont affect users at all.

What should you do?

  1. Check whether you're using userinfo
  2. Update scopes
  3. Replace userinfo API call with a call to plus.people.get

Are you using the endpoint?

Look for the section in your code where you retrieve the user's profile or email address. If you make the API call directly, you may see a URL like "https://www.googleapis.com/oauth2/v1/userinfo". The "v1" might also be "v2" as there are a couple of different versions of the api, but if it has /oauth2 then you're using the userinfo endpoint. If you're using a client library, look for using the (somewhat confusingly named) OAuth2 service or API. For example:

PHP: new Google_Service_Oauth2($client);
Python: service = build('oauth2', 'v2', http=http)
Java: oauth2 = new Oauth2.Builder(httpTransport, JSON_FACTORY, credential).build();

All of these are indicative of retrieving the user data from the userinfo API, and will need to be changed before September 2014.

What scopes should you use?

https://www.googleapis.com/auth/userinfo.profile and https://www.googleapis.com/auth/userinfo.email can be seamlessly replaced with the shorter strings profile and email. These are aliases, so they wont require the user to reconsent if they've already give access. If you're thinking about a larger change, check out my earlier post about choosing a sign in scope.

What API should you call?

All profile information for Google users is now served from the people.get API. This works for users whether or not they have a Google+ profile. It returns data in a slightly different format to the userinfo API, so you may have to change your code a little, but all the same data is available. A call to retrieve the users name, picture and email address would look like this in PHP (and should be analogous with any of the client libraries).

You can actually try the call direct from the "Try It" section at the bottom of the API page to see what the returned data looks like.

What if you want to make the smallest code change possible?

If you aren't using a client library, and don't want to change the parsing you're using, you can call the special getOpenIdConnect method, which returns data in the same format as userinfo. The only difference needed then is looking for the "sub" field rather than "id" (this was a spec change made during the development of OpenID Connect). Take a look at the differences between the calls to the API below

Userinfo API response

https://www.googleapis.com/oauth2/v1/userinfo?access_token=ya29.1.AA...
{
 "id": "104824858261236811362",
 "email": "ian@example.com",
 "verified_email": true,
 "name": "Ian Barber",
 "given_name": "Ian",
 "family_name": "Barber",
 "link": "https://plus.google.com/+IanBarber",
 "picture": "https://lh6.googleusercontent.com/-DXmOngLN6Gc/AAAAAAAAAAI/AAAAAAAAGc4/Roeci0EovY8/photo.jpg",
 "locale": "en-GB"
}
openIdConnect API response:
https://www.googleapis.com/plus/v1/people/me/openIdConnect?access_token=ya29.1.AA>...
{
 "kind": "plus#personOpenIdConnect",
 "sub": "104824858261236811362",
 "name": "Ian Barber",
 "given_name": "Ian",
 "family_name": "Barber",
 "profile": "https://plus.google.com/+IanBarber",
 "picture": "https:https://lh6.googleusercontent.com/-DXmOngLN6Gc/AAAAAAAAAAI/AAAAAAAAGc4/Roeci0EovY8/photo.jpg?sz=50",
 "email": "ian@example.com",
 "email_verified": "true",
 "locale": "en-GB"
}

In both cases, you do get a fair bit less than using the people.get API, so I would strongly recommend using people.get where possible!

There are a lot more tips on migrating to the newer sign-in options on the auth migration page, and of course if you have any questions drop into the StackOverflow tags for google-plus and google-oauth.

Tuesday, 18 February 2014

Migrating from PlusClient to GoogleApiClient

Version 4.2 of Google Play Services introduced a new replacement for PlusClient, GoogleApiClient. While PlusClient is still present, it will be removed in a future version so its a good idea to upgrade when possible. The new class has been designed based on feedback from developers across various Google Play services APIs, and provides a much more consistent, powerful base.

PlusClient was reasonably straightforward for developers implementing just Google+ Sign-In. Unfortunately, anyone looking to combine multiple services together quickly realised that managing a series of callbacks for all the different connections was way too painful. GoogleApiClient solves that by providing one central client to connect or disconnect. It allows multiple APIs to be added to a connection for Google+, Drive, Games, and the other services available.

Replacing PlusClient with GoogleApiClient isn’t too hard, and if you are using multiple clients makes life gets a lot easier - you don’t have to worry about what order to start clients in, and a much more consistent and simple workflow for calls to different services.

Building the client

The GoogleApiClient should be constructed in precisely the same place as the old PlusClient was, using GoogleApiClient.Builder. Instead of the builder taking the listener objects for the ConnectionCallbacks and OnConnectionFailedListener as constructor arguments (which usually lead to this, this, this as arguments), these are now passed in separate methods. Only the context is still passed as a constructor argument, and the others can be omitted if they aren't needed for that particular connection.

A bigger change is that each individual service has to be requested by calling addApi on the builder. No scopes will be requested by default, so most services will need a call to addScope as well. Because more than one addScope can be chained, we can avoid worrying about space separating a big string of scopes. Our basic GoogleApiClient setup to replace a straightforward PlusClient integration would look like this:

As you might guess, we’ll need to update the types of the connection failed and callbacks interfaces: GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener. You may have noticed that the onDisconnected callback is now slightly more clearly named as onConnectionSuspended. Whatever the name, this still indicates the binding to Google Play services is no longer active.

Other than that we call connect and disconnect in exactly the same way as before, such as in our onStart/onStop methods:

With the onConnectionFailed we save and resolve a ConnectionResult exactly as we would have with PlusClient. The difference is that once resolved this will connect all client services at once.

Retrieving Friends

The way to work with result sets has changed, as we can see if we look at retrieving the visible collection of friends. The new interface is much more consistently applied across the different APIs, so once you get used to it here you'll find it a familiar pattern with whichever Google service you use next. Rather than all the methods sitting on the client class, they've been divided into logical APIs, such as Plus.PeopleApi. Each method takes a GoogleApiClient as the first argument, which is used to execute the call.

The interesting thing is that we have a couple of options on what to do with the PendingResult that comes back. The old PlusClient offered a listener interface which we could implement, and the same thing can be applied with the result here.

However we can also call await on the response to get the result back in a synchronous fashion. This can be much easier if we know we will hit a point where we cannot continue without the result, and are working off the UI thread. In both cases we can test whether the call was a success by calling the getStatus method on the result, and then retrieve a buffer for the collection to iterate over as normal. Its important we close the buffer at the end.

Note: you must have enabled the Google+ API in the developer console, or you’re likely to get back a status of Status{statusCode=NETWORK_ERROR, resolution=null}.

The really cool side effect of the pending result is that we can make the call before the client is connected! If we put the loadVisible call in the onCreate and set up a callback, we’ll be pinged back as soon as the user has signed in. Because this and the other feature are part of PendingResult rather than being dependent on the API, these methods can be applied to almost any call exposed through the new Play services model, whichever product they're for.

App Activities

Reading App Activities is pretty much the same as the People example, so should be an easy port. Writing has become a little bit simpler, but we need to make sure to request the proper types of App Activity when creating the connection to ensure the user grants us permission to write them. The old addActions method is no more, so the activity type should be passed to the addApi method as in a second parameter. This will be familiar to anyone that has used GoogleAuthUtil to request offline access, except with a custom object instead of a Bundle.

The App Activity types are now set as separate strings, rather than one space separated one. We pass the resulting options object as the (optional) second parameter to our addApi call to the GoogleApiClient.Builder.

Writing app activities is pretty straightforward, but because a PendingResult is returned you can get a notification when the write happens with the same kind of callback as before if desired.

Sign out and revoke

You may have noticed that GoogleApiClient doesn’t have a methods for clearDefaultAccount, getAccountName, or revokeAccessAndDisconnect. These options from PlusClient are part of the Plus.AccountApi. The value of the consistent approach to responses really shows with something like revoking access - instead of a custom callback type to get a signal once the user is disconnected, it will return a PendingResult which you can await or setResultCallback on just as with any other API.

Using the client with multiple APIs

Part of the original motivation for this change was to make it possible to add other APIs into a single GoogleApiClient connection. There was an example of connecting both Plus and Drive in one call by adding both APIs in the post on the Android developers blog. All that is required is multiple addApi and addScope calls for the various scopes and services required.

That’s about it! As more APIs find their way into Google Play services this type of client should make life a lot easier, and much simpler to extend as you want to add more functionality to your integration. If you have any questions on moving over try the Google+ developers community, or Stack Overflow tag.

Tuesday, 14 January 2014

Choosing a Google identity scope

With all the changes to Google+ Sign-In at the end of last year, it was easy to miss some of the extended options that have been added. In particular, this update added "profile" as a valid Google+ Sign-In scope, and its not immediately obvious what the implications of choosing between the different sign-in scopes are.

To help give a bit of context to the problem, there are really only three states of users that need to be considered when choosing a scope:

  1. Google+ user: This is a Google or Google Apps user who has a Google+ profile.
  2. Non-Google+ user: This is a Google or Google Apps user that has not upgraded to Google+.
  3. Google+ disabled Apps user: This is a user of a Google Apps account where the administrator has disabled Google+.

We can take a look at the two main sign-in scopes with that in mind.

profile

The most basic sign-in scope is profile. This can be used for all three classes of users. A token will be returned through the normal OAuth 2.0 process, and you will be able to retrieve the user's profile from the plus.people.get - even if the user has not upgraded to Google+. Using this scope will not prompt the user to upgrade. The data you get back will vary depending on which bucket they fall in to though. You can use this scope with Google+ Sign-In branding, and you will still get the benefits of cross-device sign on and over the air installs of Android apps.

Using the profile scope will add a line "View basic information about your account" or similar on the consent dialog when the user is granting access to your app.

Retrieving the profile information will show a different amount of data depending on whether the user has Google+ or not. The response for my demo account looks like this. Importantly, note that isPlusUser is set to true, and the user has display name and image.

{
   "kind":"plus#person",
   "etag":"\"RVZ_f1bhF-B19rh4H4M0uhzoFng/tjequdQwl0vd7Jl18-qniz0_KfU\"",
   "gender":"male",
   "objectType":"person",
   "id":"113340090911954741696",
   "displayName":"Ian Demobarber",
   "name":{
      "familyName":"Demobarber",
      "givenName":"Ian"
   },
   "aboutMe":"This is just a demo account, please don't add me to circles!",
   "url":"https://plus.google.com/113340090911954741696",
   "image":{
      "url":"https://..."
   },
   "organizations":[
      {
         "name":"Google",
         "title":"Demo Accounter",
         "type":"work",
         "primary":true
      }
   ],
   "placesLived":[
      {
         "value":"Manchester",
         "primary":true
      }
   ],
   "isPlusUser":true,
   "language":"en",
   "verified":false,
   "cover":{
      "layout":"banner",
      "coverPhoto":{
         "url":"https://...",
         "height":624,
         "width":940
      },
      "coverInfo":{
         "topImageOffset":0,
         "leftImageOffset":0
      }
   }
}

In either the case of a user without Google+, or one who cannot upgrade due to their Apps account settings, you will get a shorter response where isPlusUser is false.

{
   "kind":"plus#person",
   "etag":"\"RVZ_f1bhF-B19rh4H4M0uhzoFng/TmQIK9e5cog4EqQDPpSv_2wJ0e4\"",
   "objectType":"person",
   "id":"104089738742024349415",
   "displayName":"",
   "name":{
      "familyName":"Barber",
      "givenName":"Ian"
   },
   "image":{
      "url":"https://..."
   },
   "isPlusUser":false,
   "language":"en",
   "verified":false
}

Pros: Lowest number of hoops to jump through, small consent line.

Cons: No access to Circles or App Activity writes, and less guaranteed profile data.

Its also worth noting that on Android (at the time of writing) if you use the profile scope with the PlusClient, the user will be taken through the Google+ upgrade flow if they haven't previously upgraded. You can use GoogleAuthUtil directly to avoid this if you need.

https://www.googleapis.com/auth/plus.login

This is the main Google+ Sign-In scope. It enables access to full the full range of Google+ features, but requires users to have a Google+ profile. The practical implication of this is that users may get an extra step in their consent flow asking them to upgrade the first time they sign in to an application that requests plus.login. There's no value in using both together, so if you ask for plus.login, you can remove profile.

This will ask for consent to "Know your basic profile info and list of people in your circles” and "Allow Google to let the people in these circles know that you have signed in to this app with Google” (or some variation), in each case with the ability to select different circles.

Each of the classes of uses will get slightly different behaviour with plus.login:

  1. Google+ users: See consent dialog, then sign in.
  2. Non-Google+ users: See upgrade page, then consent dialog, then sign in.
  3. Google+ disabled Apps user: See consent dialog, then sign in, but resulting token is automatically down-scoped.

The last one there is the most interesting case. Because users on an apps domain which has Google+ disabled cannot upgrade, they aren’t shown an upgrade screen. Instead of blocking them from signing in at all, the resulting token is effectively limited to a profile style scope. This means that you should allow for the possibility of not being able to write App Activities or retrieve friends of a user. In these cases you will receive a 403 error back, which you could use to mark that you should not attempt further calls of that type for that user (see the note in the auth migration doc for more). You can also check the isPlusUser field in the plus.people.get response (as above) in order to see whether you should be able to make such calls.

The profile response for a fully upgraded user looks similar to above, but also contains the an age range field. Using the scope will also mean that the vast majority of users will have Google+ profiles, with the associated richer profile.

{
   "kind":"plus#person",
   "etag":"\"RVZ_f1bhF-B19rh4H4M0uhzoFng/1azU0ZyG8HpqnhDWPNjBuLksUJ0\"",
   "gender":"male",
   "objectType":"person",
   "id":"113340090911954741696",
   "displayName":"Ian Demobarber",
   "name":{
      "familyName":"Demobarber",
      "givenName":"Ian"
   },
   "aboutMe":"This is just a demo account, please don't add me to circles!",
   "url":"https://plus.google.com/113340090911954741696",
   "image":{
      "url":"https://..."
   },
   "organizations":[
      {
         "name":"Google",
         "title":"Demo Accounter",
         "type":"work",
         "primary":true
      }
   ],
   "placesLived":[
      {
         "value":"Manchester",
         "primary":true
      }
   ],
   "isPlusUser":true,
   "language":"en",
   "ageRange":{
      "min":21
   },
   "verified":false,
   "cover":{
      "layout":"banner",
      "coverPhoto":{
         "url":"https://...",
         "height":624,
         "width":940
      },
      "coverInfo":{
         "topImageOffset":0,
         "leftImageOffset":0
      }
   }
}

What about some of the others?

email (or https://www.googleapis.com/auth/plus.profile.emails.read) - This scope requests access to the users email address, which is included in the plus.people.get response as above, under an emails key.

"emails": [
  {
   "value": "me@example.com",
   "type": "account"
  }
 ]

The primary verified email will always have the type "account". The emails value is always a JSON array, and there may be more than one address returned in some cases.

https://www.googleapis.com/auth/plus.me - This scope allows replacing the Google+ user ID with "me". As that makes it trivial to call plus.people.get for the current user, it will cause a line on the consent dialog that refers to profile information. In pretty much every case this should be replaced with the profile scope - at the moment the only place you should use plus.me is if there is a legacy requirement on in it in the specific API (such as, at time of writing, with the Google+ Domains API).

https://www.googleapis.com/auth/userinfo.email and userinfo.profile - Both of these are superseded by the profile and email scopes, so you should be able to drop them anywhere they are still used. Note that the old userinfo API endpoint is now actually be handled by the Google+ services, so there is no need to continue hitting the user info API at all - plus.people.get is the best choice in all cases.

Incremental

Finally, its worth pointing out that you don't always have to choose. You could start with profile, and when users take an action that requires the Google+ data ("find my friends" or similar), prompt for an incremental upgrade to https://www.googleapis.com/auth/plus.login. This is more work, but may be a solution for those that want the simplest consent screen without giving up access to other features.

Wednesday, 18 December 2013

Incremental Auth and YouTube scopes

In my previous post I mentioned that there are two issues which have been made more visible by incremental auth. The first of these is fairly straightforward, but the second is a little more subtle. Incremental auth is a great feature for simplifying the consent screen that users see when they first sign in to an app, but it can also introduce a bit more complexity in some cases. An example of this is when requesting access to YouTube.

Because YouTube profiles support delegated access to Google+ pages, their data can be associated with these pages as well as general Google accounts. Whenever you request access to a YouTube scope (even in combination with other scopes), the user will have the opportunity to choose one of their pages if they have any. Currently this only occurs on the web, and will result in the user seeing a screen like this:

So far, so good - everything works as expected. Where it can get tricky is that if you ask for a YouTube scope incrementally, you need to account for the possibility of the user choosing a Google+ page when previously they had selected their Google+ profile. This is particularly common for people that merged their YouTube channel to their Google+ profile, but decided to keep the old username.

In this situation, its important to check the user ID associated with the returned token, and make sure that you can distinguish between the two. If the user loads another page that only requests the base scopes again (e.g. without YouTube), any immediate auth check (such as triggered by page level config) will return the user originally selected.

What do I need to do?

If dealing with YouTube, the best thing to do is allow for the case where the user ID changes after a YouTube incremental auth request. This means keeping the user ID around. In a client server situation this is relatively easy to do by keeping the user ID in a session or similar, but in the sample code here we'll store it in a local Javascript value.

When offering the incremental option, it may be easiest to always treat the token as separated. You could even set include_granted_scopes/includegrantedscopes to false in order to request only the minimal scopes required, if scanning the user's watch history for example. In the sign-in callback you can check whether the user ID of the new user is different than the existing one. If so, you can send a fresh code to your backend server, or immediately use the token to make the YouTube API calls.

Note: The ID token unpacker here is used for convenience, and shouldn't be used for any security functions - if you need to trust the user ID value then send the entire ID token to the server side, where the certificate should be validated!

If sent to the server, the resulting token should be stored this along side the existing one as an additional YouTube-specific token. Any YouTube API calls should be made using this token, falling back to the main token if no YouTube token is present. In a disconnect call, both tokens should be revoked. This is a bit more complex, but means you can support authentication being delegated without getting into a confusing situation with two users for the same account!