Google+ Android Client & Server Sign-In
NOTE: This blog post is pretty old! Take a look at a much better way of getting an authorization token or check out the official Google Sign In documentation for a more up to date (and easier!) choice.
Google+ Sign-In is a really easy way to authenticate and identify a user and build interesting experiences on Android. Some apps needs more than just an authenticated Android client though - they also need to make Google API calls from their server, perhaps even when the client is offline. Enabling both to access the APIs at the same time is slightly tricky, but in this post I'm going to try and break down what is required, and how you can do this in a robust and reliable fashion. Note that life is much easier if you just need either the client or the server to have API access, or just need to identify the user to a server. This approach is solely for the case where you make Google API calls on the Android client, and also Google API calls from the server.
Our model is that we have an application which needs to have authorisation to call some Google API, and a server which also needs authorisation, and continues to need access when the user isn't there. To achieve this the server stores a refresh token in some kind of database, which it can look up by the Google user ID. It also creates a session between the client and server, which is then used to make application (non-Google) API calls. Our goal is to have every user have a signed-in client, a refresh token on the server, and a session between them. When signing in on Android, we can choose to retrieve a code and send it to the server, but we have to be careful to avoid showing the user 2 consent screens, or exchanging the code for a refresh token when we already have one.
There are quite a few moving parts involved with various callbacks, as so much of the process is asynchronous. The best way I've found to think about this process is based on the scenarios of the users, which is how we'll look at the implementation. In each case, we progress through gathering additional information with a series of PlusClient connections and server calls until we arrive at the state where both client and server are signed in and have a shared session.
We'll go through 3 scenarios: new users, who have never signed in to your service, existing users who are returning to your application, and users who have logged out. Then, we can tie them all together with a bit of error handling. We'll start out with a basic Android activity as setup for Google+ Sign-In, with some state variables we will use to track the flow:
New Users
A user who is accessing the app/service for the first time will have no stored refresh token with the server, and so will certainly need the code. We start with setting up our PlusClient as normal, and triggering the connect in our onStart. In this case we'll get a call to onConnectionFailed, with a connection result which we can store for later.
Our onConnectionFailed just stores the connection result for the time being.
We can kick off the connect in the onStart method as normal, but we don't want to do anything right away, as the user hasn't chosen to sign in yet. Therefore, the magic actually happens in our onClick listener for the sign in button. In the normal flow we'd resolve the stored ConnectionResult now, but we want to retrieve a code instead so. We need to establish which user the wants to sign in with, so we can use the AccountPicker from Google Play Services:
This returns to our onActivityResult. Now we know which account the user wants to sign in with, we can retrieve the code and send it to our server.
Don't worry, we'll cover getCode in a bit. The important thing is that this is the end of the flow! At the end of getCode its going to trigger a reconnection of PlusClient, which will then connect us through the magic of cross platform seamless sign on. We'll build on this base as we address the other cases.
Returning Users
Returning users are awesome! We will get the seamless sign-in, so we can expect a PlusClient onConnected event. There are in fact two places we might get returning users - users that have used the app on this device before, and users who are seamlessly signing in having previously signed in on the web or another device. So, we just need to establish a session with the backend for these uses in the onConnected method if we don't already have one.
Note that we retrieve the account name from the PlusClient if we didn't already have it. Because of this, we will need the GET_ACCOUNTS permission in our Android manifest. If we don't have a session with the server, we'll establish one. This will also reconnect us, at which point we'll know we're fully signed in and can hide the sign in button and take other such actions. At that point, we have a session on the server, and both server and client authorised to make Google API calls.
Signed Out Users
We should offer sign out in our application. Usually this will be a method like this:
Note that we clear our prefs account name and stored session cookie as well as the PlusClient's account. If a user returns in this state, the server will have an refresh token for them, but the local client won't be connected. In that case, we just need to have them pick an account and all will be good.
Because both of these are going to be triggered by a user click and an account choice, our client needs to be a bit smarter. Now, in our activity result, we will treat it like this:
So we will initially try to retrieve a session. At the end of the session retrieval, we will reconnect. If that works, we're back in the returning users flow. If, however, the server does not have a refresh token, we won't be able to start a session. In that case, we'll get an onConnectionFailed while we have a set account name, and a known server session state. If we find ourselves in that condition, we can fetch the code. Note the hash we're using to track the server refresh token state - we'll fill that in createSession below.
The getCode will then trigger a reconnect, and we'll sign in as in the new user flow.
Get Session
One of the two main questions is how do we securely establish the session with the server. We do this by sending across the ID token for the account that has been selected. This allows the server to securely verify the Google user ID, check in its database whether it has a refresh token stored, and whether that refresh token is still valid.
There are two parts to the getSession functionality - retrieving the ID token, and sending it to the server. Both may involved network access, so these should be done off the UI thread. We'll create a private AsyncTask class to contain the fetching logic. We'll also need to define another constant - the client ID of the server that will be processing the ID token. Our getSession method will therefore just check whether there is a an existing session, and kick off the task if not.
The first part is just retrieving the ID token with the account name we have retrieved. We just need to pass a scope string with this particular formatting to the getToken method in GoogleAuthUtil, along with the client ID for the web server we will be sending it to. Retrieving the ID token doesn't trigger any user visible interaction.
You'll note we aren't doing a lot with the error handling here. There is a lot of scope for making a nicer user experience by managing these different cases. Particularly prompting the user to install Google Play Services, and so on. These will all be the same as in the regular Google+ Sign-In on Android implementations though.
Next we send the retrieved ID token to our server. Even though an ID token can't be used to access Google APIs, it should always be sent across HTTPS. This ensures it can't be snooped on by middlemen, and perhaps used to impersonate the user to your server. On the server side, we need to verify the ID token, extra the user ID, check whether there is a refresh token, and that the refresh token is still valid (generally by creating an access token, or checking a cached one). If that is true, then we can generate a session cookie and return it as the body of a response with a 200 request (we could of course return it in a header as well). Otherwise, the server returns a 401 to indicate it does not have a stored token.
In real applications it is preferable to do this type of checking on regular API calls, rather than having a separate session connection call as in this one. In this little sample though, we don't have any other API calls!
At the end of the async task we add a post execute method. If the retrieve was successful, we mark the status of that user on the server in the hashmap (indicating they do or don't have a refresh token on the server), and rebuild the PlusClient to be sure it includes the default account name. We then reconnect it, in order to trigger the next step.
At this point we will either get onConnected and be on our way, or we will get an onConncectionFailed need to get a refresh token for the server via the getCode method.
Get Code
Finally, we get to the part where we actually retrieve the code. You might be asking, why all the roundabout work beforehand? There are two constraints around managing the code:
- Retrieving a code will almost always display a consent dialogue.
- There is a limit to the number of refresh tokens a user may have for an application.
- For seamless sign in, the server must have exchanged the code for a token, and all scopes must match.
This means that if you were to get a code each time, you'd have to show the user the consent dialogue every time they signed in. It also means that if you exchanged that code each time, and never revoked any of the previously exchanged tokens you would eventually hit a limit where you couldn't retrieve any more tokens.
Just like with getSession we'll need to make a GoogleAuthUtil.getToken call to get the code, and an HTTPS call to the server to exchange it and set up a session. There is an extra complication in that we will have to deal with the consent screen. This will be shown if the user has not approved a consent screen for the code within a fairly short time span. We'll need to catch a UserRecoverableException and fire the Intent to allow the user to approve the application.
Note that here we are requesting just plus.login, but in general apps will want more than one scope, and these must match in all places. One common error when implementing multi-app systems is to have differing scopes, so its well worth defining these in a single place, and always referring to that string!
The normal flow when this code is executed is that it will throw the UserRecoverableAuthException. There will be a short period after retrieving the code where the code will be returned automatically, but that won't happen if the consent dialog was for a regular ConnectionResult resolution. We'll have the user sign in, and handle the result in the onActivityResult. All we need to do is fire the getCode task again, where this time the code retrieval should succeed:
When we need to get the code itself, we need to do something with it. Before we can reconnect we need to have the server exchange the code for a token. Before it has done this, we won't get seamless sign-in, which will mean the user would have to consent again. So, the server exchanges and sets up a session cookie for us at the same time.
Note: When exchanging the code on the server side, you don't need to pass a redirect_uri!
You'll note that we invalidate the code as soon as we have sent it. Whether it worked on not for the server, we want to consider that code burned. If we don't invalidate and go through the flow again, we'll just receive the same code back due to the local caching within Google Play Services. This is likely to cause some confusing errors! Calling this function requires the USE_CREDENTIALS Android permission.
In our post execute step we want to do exactly the same as on the ID token session retrieval, as we are expecting the same cookie back in the body. Once again, we will trigger a reconnection of the PlusClient. Since we have an existing session, cross client single sign on should fire and we will be smoothly signed in.
One More Thing
That should be the main error cases, but for comfort it would be good to cover the situation where you have a session, but for some reason PlusClient still results in onConnectionFailed. In that case, all we can do is resolve the ConnectionResult and deal with it like a regular Google+ Sign-In. This is most likely to happen because the user has got a refresh token from another client (such as web), but the scopes don't match the ones requested on Android.
To account for this, we can add a little code to our click handler:
And then in the activity result handle the new result - as always, just reconnecting the PlusClient.
Hopefully that gives you an example of what you can do! There's the full activity from this post all in one go in this Android Google+ sign-in hybrid flow gist as well - though this type of flow is so application specific please just don't drop it into your app and hope! For a more realistic example, watch out for samples coming soon as part of the official Google+ documentation.
Appendix
I actually don't think this is too complicated once you get it in code, but it is not the easiest flow to explain. If you come up with a better way, please blog it and let me know! For your amusement though, before writing this post I tried to write down the flow in a sensible way - it took me quite a few goes (the way its explained here is the diagram on the right):