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.