Client-Server Authentication with ID tokens

These days there are few purely client-side applications - even traditionally unconnected software like casual games make use of servers and services to provide a better, richer experience - and importantly one that can follow the user across devices. We are in a world where we need to authenticate a user of many clients to a set of back-end services.

There are a few ways of doing that when using a identity provider, like Google, but in this post I want to talk about a specific method that makes use of ID tokens. This approach has been well covered by Tim Bray for Android, but with the release a couple of months ago of the iOS 1.4.0 SDK for Google+, it is now available across Android, iOS and the Web. This makes it a particularly powerful way of signing in a user, and asserting the identity of that user securely to your application servers.

Why ID Tokens

This whole architecture relies on a point of view which I suspect is still not particularly common amongst developers implementing authentication. We generally thought of social sign-in as a way of getting access to your identity and a bunch of realted service - so that you use social sign-in in the Android app, pass something to your server, and they could each take actions on the network: maybe the server writes to your social network, the client retrieves a profile picture (for example).

ID tokens are based on the idea that your server primarily focuses on providing services to your own clients. In this model, you authenticate your client, use that to identify the user to your server, and make any social API calls from the client alone. The server provides application specific calls, and if data sourced from the social network is needed, it can be passed via the clients. This, in general, leads to a pretty clean separation of concerns and a secure system. The ID token is pure identity, not authorisation - it doesn't grant the holder any ability to make API calls. It does contain information about them though, so it must only be sent over HTTPS.

This style is pretty similar to how we used to build systems with usernames and passwords, particularly on mobile. When the user starts an app, they were prompted to sign-in, and the username and password were sent to the server to authenticate them. After that, a session cookie was used form a connection between client and server. The client then would take advantage of services on the mobile application, such as location or the camera, and pass data as needed to the server. With ID tokens we take the same model, except instead of a username and password we pass a cryptographically signed assertion of who the user is, and as well as services like location and the camera we include calls to retrieve user information from Google APIs. In pictures, something like this:

ID tokens are part of the OpenID connect spec. If you receive an ID token you can verify who the user is, which app they are using, and who is asserting that. For example, if my server receives an ID token from a client, it can get the Google user ID, email address (if the user has granted permission on the email address to the app), the client ID the token was generated for, and who generated it. The validity of those statements can be checked cryptographically. The token is small enough to pass around with calls, and easy to retrieve and to verify.

Using an ID token

Here is what an ID token looks like:

eyJhbGciOiJSUzI1NiIsImtpZCI6ImFlOGU0NmMzN2UzOWMzY2ZiMTgxNWI2YjU4MmM2MTNiOTk0N2MxZTQifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXRfaGFzaCI6InFmdXFud3NXWWRzLXBzMW1oYkU5cXciLCJhenAiOiIzNjY2NjcxOTE3MzAtajRmdThwbnZjMmoxdHRrcmwxazdqdTVuNXBpbTN2ZXQuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIzNjY2NjcxOTE3MzAtajRmdThwbnZjMmoxdHRrcmwxazdqdTVuNXBpbTN2ZXQuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTMzNDAwOTA5MTE5NTQ3NDE2OTYiLCJpYXQiOjEzODMxNDc0MjEsImV4cCI6MTM4MzE1MTMyMX0.Z2NVE5HQxsLXx_zRmG3bxgGPDUv76HffDYvhlU_OpgLDeeIxQnC7cAS2OkAUK-nkDci3rMTM035NeTQUKfHsUziOV_WGyDtuRq_KEBDev0ssr8EeTq0Wg-nYN8eo6nbfKYTtd4UnOMG-xYetyyPIN8SNy3G7P1Aw3CakhbD32I0

There are 3 dot separated sections, each base64 encoded. The second part is the payload, which looks like this. You can see the Google user ID (sub for subject), the creator of the token (iss for issuer), and the Google console client ID that the token was generated for (aud for audience).

 {
  "iss": "accounts.google.com",
  "at_hash": "qfuqnwsWYds-ps1mhbE9qw",
  "aud": "132456789654.apps.googleusercontent.com",
  "sub": "113340090911954741696",
  "iat": 1383147421,
  "exp": 1383151321
}

The first and third parts are part of the security mechanism. The third part is the signing hash - a digest of the first two parts created by Googles private keys. We can check that against Google's public certificates, which are rotated every few hours. The Google certificates are normally found on a standard URL, and are in a JSON structure with an identifier and the certificate in x509 format. The first part of the ID token identifies the algorithm used, and the identifier of the certificate used to generate the signature.

eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk1YTNjMjkwNTI4NGZlMTE3OGI1OTJjOTg4YWYyYjM5YjNjZTU4ZWQifQ {"alg":"RS256","kid":"95a3c2905284fe1178b592c988af2b39b3ce58ed"}

The signature part is verified by checking the signature against the public key. This is calculated over the first two parts of the ID token, in base64 encoded format. If this passes, we know that the signature was created across all the data in the ID token by the Google private key, so we can be sure of the source of the message.

Server Side

The normal setup for a system of this nature is to piggy-back the autentication information on the regular calls. This minimises round-trips, and helps to separate the different responsibilities in code. We will work on the basis that we will send the ID token to bootstrap a random session identifier, though it is also a valid design to simply pass the ID token along with every request - the cost being that is somewhat larger than a common session cookie, and takes more work to verify.

If the session cookie has expired or the client doesn't have one, the server indicates that it requires authentication. The client then repeats its request, with an ID token - in the example code below the token is sent as a GET parameter, but a more sensible design would probably pass it as an Authentication: Header. Similarly, the session is returned using the standard PHP session mechanism, but there are many methods for passing a token on calls.

Our server, in PHP, might look something like this:

Note the verifyIdToken function here. That is calling the Google PHP client library, and automatically manages retrieving the certificates from Google, checking the validity of the ID, and extracting the payload data. If you want to see all the details of the checks made, you can look directly at the client library function. Client libraries in other languages also include simple verification functions for ID tokens.

Retrieving in Javascript

The normal implementation across all platforms is to setup regular Google+ Sign-In, and in the "connected" callback retrieve the ID token and send to the server. Retrieving the ID token in Javascript is not more complicated than that. When the user signs-in the callback will fire with the normal access token, and so on. As part of that dictionary, the ID token is provided.

Retrieving on iOS

On iOS you must be using version 1.4.0 or above of the SDK. Once the user has signed-in you can retrieve the ID token at any time using the GPPSIgnIn sharedInstance.

While most of the time on the web we will be generating an ID token for the same client ID as on the server, with iOS and Android we will have separate client IDs. We can specify that we would like to generate a token for another client ID as long as it is in the same API console project as the client ID we are signed in to. You can see this with two different values in the azp (authorized party) and aud fields:

{
  "iss":"accounts.google.com",
  "cid":"911581857976-tbl0ni100qvqbjmrbkc2uo6jdure3k1h.apps.googleusercontent.com",
  "azp":"911581857976-tbl0ni100qvqbjmrbkc2uo6jdure3k1h.apps.googleusercontent.com",
  "email":"ian.example@example.com",
  "id":"104824858261236811362",
  "sub":"104824858261236811362",
  "verified_email":"true",
  "email_verified":"true",
  "aud":"911581857976.apps.googleusercontent.com",
  "iat":1384436584,
}

This example also includes the email address, as that scope had been requested as part of the sign in process. You may also note there are a few duplications of fields - this is due to supporting both the current and previous iteration of the OpenID connect standard.

Android

Tim Bray's article is again the definitive source, but in general retrieving an ID token when you have a signed-in user is really straightforward. The only complexity on Android is that you have to make the actual call in an AsyncTask as it may perform a network operation:

Again, we specify the client ID of the home server, for easier verification. For completeness we must include a number of exception handling cases which is reality will not trigger - the getToken method on GoogleAuthUtil is overloaded to return access tokens and codes as well as ID tokens, depending on the scope parameter passed.

Hopefully you can see that there is some use in verifying backend calls using ID tokens. The fact that it can be used across the three primary platforms with Google+ Sign-In makes it a great way to consider building your system if that is your sign-in mechanism. Because its sending a token to set up a session, its very compatible with other methods of authentication - conceptually its the same idea as sending a username and password when the user has no session, but more secure!

Be sure to always send ID tokens over HTTPS - though they can't be used maliciously in themselves, an attacker could use one to establish themselves a session with your app server if they could intercept it, so its important they aren't send in plain text.

Popular posts from this blog

Common problems with Google+ Sign-In on Android

Upgrading Firebase.com & Using Google Sign In on Android