Consuming a Google ID Token from a server

Before your server can trust that a Google ID Token actually comes from a valid user, you need to validate it. Validation of an ID token requires two steps:

  • Verify that the value of the aud field in the ID token is identical to your app’s client ID and that the iss is accounts.google.com
  • Verify that the ID token is a JWT which is properly signed with an appropriate Google public key and has not expired

Anatomy of an ID Token

An ID Token consists of three sections separated by dots: header.body.signature. Here is an example taken from Google:

1
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxNDIwNDk1MzA5NDM1MjE2ODU3MSIsImF1ZCI6Ikdvb2dsZSIsInR5cCI6Imdvb2dsZS9wYXltZW50cy9pbmFwcC9pdGVtL3YxIiwiaWF0IjoxMzg1MDc2MTM4LCJleHAiOjEzODUwODIxMzgsInJlcXVlc3QiOnsibmFtZSI6IlBpZWNlIG9mIENha2UiLCJkZXNjcmlwdGlvbiI6IkEgZGVsaWNpb3VzIHBpZWNlIG9mIHZpcnR1YWwgY2FrZSIsInByaWNlIjoiMTAuNTAiLCJjdXJyZW5jeUNvZGUiOiJVU0QiLCJzZWxsZXJEYXRhIjoiWW91ciBEYXRhIEhlcmUifX0.psOU3HlUMGjK_auKEkBhSLzi5n2ATUtaxn_XItGvdhA

The information on this token is not really secret. You can easily get the value for the header and body using node:

1
2
3
4
5
6
var token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxNDIwNDk1MzA5NDM1MjE2ODU3MSIsImF1ZCI6Ikdvb2dsZSIsInR5cCI6Imdvb2dsZS9wYXltZW50cy9pbmFwcC9pdGVtL3YxIiwiaWF0IjoxMzg1MDc2MTM4LCJleHAiOjEzODUwODIxMzgsInJlcXVlc3QiOnsibmFtZSI6IlBpZWNlIG9mIENha2UiLCJkZXNjcmlwdGlvbiI6IkEgZGVsaWNpb3VzIHBpZWNlIG9mIHZpcnR1YWwgY2FrZSIsInByaWNlIjoiMTAuNTAiLCJjdXJyZW5jeUNvZGUiOiJVU0QiLCJzZWxsZXJEYXRhIjoiWW91ciBEYXRhIEhlcmUifX0.psOU3HlUMGjK_auKEkBhSLzi5n2ATUtaxn_XItGvdhA';
var parts = token.split('.');
var headerBuf = new Buffer(parts[0], 'base64');
var bodyBuf = new Buffer(parts[1], 'base64');
var header = JSON.parse(headerBuf.toString());
var body = JSON.parse(bodyBuf.toString());

The value for the header is:

1
{ alg: 'HS256' }

The value for body is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  iss: '14204953094352168571',
  aud: 'Google',
  typ: 'google/payments/inapp/item/v1',
  iat: 1385076138,
  exp: 1385082138,
  request:
  {
    name: 'Piece of Cake',
    description: 'A delicious piece of virtual cake',
    price: '10.50',
    currencyCode: 'USD',
    sellerData: 'Your Data Here'
  }
}

As you can see, this data can be easily faked by anybody. That is the reason the signature is very important. The signature helps us make sure that the token was actually generated by Google and not by some attacker.

Verifying aud and iss fields

Validating the the aud and iss fields is very easy once you have access to the body. Something like this will do:

1
2
3
if (body.aud !== '<your_client_id>' || body.iss !== 'accounts.google.com') {
  throw 'Id token is not valid';
}

Verify token signature

The most important step to verify that the token is valid is to check the signature. Checking the signature involves a few steps:

– Retrieve the discovery document from https://accounts.google.com/.well-known/openid-configuration
– Parse the JSON document and retrieve the value of the jwks_uri key
– Retrieve public keys from jwks_uri
– Use public keys to validate signature

Lets look at an example. Say we get this ID token from an app:

1
eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg0NzJjNjU5MGIxNzc4ZmU1MjljMWJkM2E4ZjE4MWNjMmFmNGIyMDAifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTExMzk1NDM5MjY3Mjk4MzQ3MTgyIiwiYXpwIjoiMTA5NzIwNTk2OTQzMy1qY2loYzMybHR1MjZsdXJkdXFqcXNmYmU0czhnNHVqNS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoiYWRyaWFuLmFuY29uYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiMTA5NzIwNTk2OTQzMy1mYThhNGllYWE1dmNuaHByZzcycnV0a2EyMnZwcWNsNC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTQyMjMyMzI2NiwiZXhwIjoxNDIyMzI3MTY2fQ.jTuABA2gqdPBvAZKeUI5HP2VzxJij_MkQ7A-2t1rMvgrkWetE3R0ejJXiiBXXAceqenDzeNt5bddLuG1kh_K24jgSMf37wuU2xBLeVZXq88iAAQ-iEJ0P1zsGe71tg3NRwI_tCe4qWAnPC_uT9v2daa0zNiTN15KRAIFRtlqrdE

The first step is to hit https://accounts.google.com/.well-known/openid-configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
 "issuer": "accounts.google.com",
 "authorization_endpoint": "https://accounts.google.com/o/oauth2/auth",
 "token_endpoint": "https://www.googleapis.com/oauth2/v3/token",
 "userinfo_endpoint": "https://www.googleapis.com/plus/v1/people/me/openIdConnect",
 "revocation_endpoint": "https://accounts.google.com/o/oauth2/revoke",
 "jwks_uri": "https://www.googleapis.com/oauth2/v2/certs",
 "response_types_supported": [
  "code",
  "token",
  "id_token",
  "code token",
  "code id_token",
  "token id_token",
  "code token id_token",
  "none"
 ],
 "subject_types_supported": [
  "public"
 ],
 "id_token_alg_values_supported": [
  "RS256"
 ],
 "token_endpoint_auth_methods_supported": [
  "client_secret_post"
 ]
}

Then we need to hit https://www.googleapis.com/oauth2/v2/certs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
 "keys": [
  {
   "kty": "RSA",
   "alg": "RS256",
   "use": "sig",
   "kid": "16f8295b2370dd88de4c22aed85cabf52d2757f5",
   "n": "tL3TtFEjtVIlFrMsyRncvS6ZLDRr6PejeQv7hx1k-oX0599OTYA4FQE8YYX4z95_NaQXx833DPay7KVzw751kHJz9eiSYyZmYFMM786E-PspFvdJMhU2ZCLgxLUXZ_Gq7ORgxHkJHcBWR8HstjI3zpWAOhfqg8YvSnMeOStQ1Ns=",
   "e": "AQAB"
  },
  {
   "kty": "RSA",
   "alg": "RS256",
   "use": "sig",
   "kid": "8472c6590b1778fe529c1bd3a8f181cc2af4b200",
   "n": "rIVm3h1WGbvKjmvzrpwPFeyAWIeP3W87z-C9k0YarePIF0Y77KgaMB83cVv5Hp85Che-Z_nb_y0kBhrOha4_q_6gFEOhyz8PUZSzdY2zkhX8Dci-vic9HulL5cFWjDGPXwekHLm_EmXkPkKu7-6nbkxmwcVQMGX2lEeawCqqNmk=",
   "e": "AQAB"
  }
 ]
}

This last JSON comes in a format called JWK(JSON Web Key). I am not going to go in depth about this format but it is important to understand it so we can use the key to verify the signature.

This JSON contains an array of keys. The way we know which key we should use is by using the kid(Key ID). If we decode the header part of our example token we will get this:

1
2
3
4
{
  alg: 'RS256',
  kid: '8472c6590b1778fe529c1bd3a8f181cc2af4b200'
}

We can see that the kid in the header matches the second key in the JWK. This tells us, that is the key we should use to verify.

Now that we know which key to use, we need to identify the key type(kty) and algorithm(alg). In the example we can see that we are using RSA with RS256. The JSON Web Algorithms specification dictates that an RSA public key need to also have a modulus(n) and an exponent(e).

This key can’t be used directly to verify the token signature. First we need to transform it to a PEM key. I’m not going to explain how to do this because there is already a node module that can do it for us called rsa-pem-from-mod-exp. Using the npm module is very easy:

1
2
3
4
5
6
var getPem = require('rsa-pem-from-mod-exp');

var modulus = 'rIVm3h1WGbvKjmvzrpwPFeyAWIeP3W87z-C9k0YarePIF0Y77KgaMB83cVv5Hp85Che-Z_nb_y0kBhrOha4_q_6gFEOhyz8PUZSzdY2zkhX8Dci-vic9HulL5cFWjDGPXwekHLm_EmXkPkKu7-6nbkxmwcVQMGX2lEeawCqqNmk=';
var exponent = 'AQAB';

var pem = getPem(modulus, exponent);

The resulting PEM looks like this:

1
2
3
4
5
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAKyFZt4dVhm7yo5r866cDxXsgFiHj91vO8/gvZNGGq3jyBdGO+yoGjAf
N3Fb+R6fOQoXvmf52/8tJAYazoWuP6v+oBRDocs/D1GUs3WNs5IV/A3Ivr4nPR7p
S+XBVowxj18HpBy5vxJl5D5Cru/up25MZsHFUDBl9pRHmsAqqjZpAgMBAAE=
-----END RSA PUBLIC KEY-----

With this information we can use node’s crypto libraries to verify the signature:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var crypto = require('crypto');
var base64url = require('base64url');

var publicKey = '-----BEGIN RSA PUBLIC KEY-----\n' +
        'MIGJAoGBAKyFZt4dVhm7yo5r866cDxXsgFiHj91vO8/gvZNGGq3jyBdGO+yoGjAf\n' +
        'N3Fb+R6fOQoXvmf52/8tJAYazoWuP6v+oBRDocs/D1GUs3WNs5IV/A3Ivr4nPR7p\n' +
        'S+XBVowxj18HpBy5vxJl5D5Cru/up25MZsHFUDBl9pRHmsAqqjZpAgMBAAE=\n' +
        '-----END RSA PUBLIC KEY-----\n';
var content = 'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg0NzJjNjU5MGIxNzc4ZmU1MjljMWJkM2E4ZjE4MWNjMmFmNGIyMDAifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTExMzk1NDM5MjY3Mjk4MzQ3MTgyIiwiYXpwIjoiMTA5NzIwNTk2OTQzMy1qY2loYzMybHR1MjZsdXJkdXFqcXNmYmU0czhnNHVqNS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoiYWRyaWFuLmFuY29uYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiMTA5NzIwNTk2OTQzMy1mYThhNGllYWE1dmNuaHByZzcycnV0a2EyMnZwcWNsNC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTQyMjMyMzI2NiwiZXhwIjoxNDIyMzI3MTY2fQ';
var signature = 'jTuABA2gqdPBvAZKeUI5HP2VzxJij_MkQ7A-2t1rMvgrkWetE3R0ejJXiiBXXAceqenDzeNt5bddLuG1kh_K24jgSMf37wuU2xBLeVZXq88iAAQ-iEJ0P1zsGe71tg3NRwI_tCe4qWAnPC_uT9v2daa0zNiTN15KRAIFRtlqrdE';
signature = base64url.toBase64(signature);

verifier = crypto.createVerify('RSA-SHA256');
verifier.update(content);
verifier.verify(publicKey, signature, 'base64');

The call to verifier.verify will return true only if the signature is valid.

Putting it all together

I explained how to do all the steps for verifying an ID token manually because I wanted to understand how everything works. I recommend instead of implementing all the parts yourself you use jws-jwk which makes things a lot easier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var jws = require('jws-jwk');

var jwk = {
 "keys": [
  {
   "kty": "RSA",
   "alg": "RS256",
   "use": "sig",
   "kid": "8472c6590b1778fe529c1bd3a8f181cc2af4b200",
   "n": "rIVm3h1WGbvKjmvzrpwPFeyAWIeP3W87z-C9k0YarePIF0Y77KgaMB83cVv5Hp85Che-Z_nb_y0kBhrOha4_q_6gFEOhyz8PUZSzdY2zkhX8Dci-vic9HulL5cFWjDGPXwekHLm_EmXkPkKu7-6nbkxmwcVQMGX2lEeawCqqNmk=",
   "e": "AQAB"
  }
 ]
};
var signedData = 'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg0NzJjNjU5MGIxNzc4ZmU1MjljMWJkM2E4ZjE4MWNjMmFmNGIyMDAifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTExMzk1NDM5MjY3Mjk4MzQ3MTgyIiwiYXpwIjoiMTA5NzIwNTk2OTQzMy1qY2loYzMybHR1MjZsdXJkdXFqcXNmYmU0czhnNHVqNS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoiYWRyaWFuLmFuY29uYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiMTA5NzIwNTk2OTQzMy1mYThhNGllYWE1dmNuaHByZzcycnV0a2EyMnZwcWNsNC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTQyMjMyMzI2NiwiZXhwIjoxNDIyMzI3MTY2fQ.jTuABA2gqdPBvAZKeUI5HP2VzxJij_MkQ7A-2t1rMvgrkWetE3R0ejJXiiBXXAceqenDzeNt5bddLuG1kh_K24jgSMf37wuU2xBLeVZXq88iAAQ-iEJ0P1zsGe71tg3NRwI_tCe4qWAnPC_uT9v2daa0zNiTN15KRAIFRtlqrdE';

jws.verify(signedData, jwk);

Another thing you should consider when implementing this in production is to cache the JWK document from Google so you don’t have to make the request every time.

  1. Hi,
    did you manage to get the google discovery document from an ajax request ? or how did you get it. when trying from ajax request I get cors errors so I wonder if you only checked the file using your browser ? below the error i get:
    XMLHttpRequest cannot load https://accounts.google.com/.well-known/openid-configuration. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:1485’ is therefore not allowed access.

    • adrian.ancona

      Hello Cedric,

      I request the discovery document from my server so CORS doesn’t apply. My app gives my server an ID Token and then it verifies the signature.

      • OK, do you know a way to do it from javascript on the client side ? I would like to configure my endpoint from this discovery document instead of harcode them all

        • adrian.ancona

          You won’t be able to access that domain directly from the browser. An alternative would be to create a proxy end point on your domain that makes the request for you.

Leave a Comment


NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Time limit is exhausted. Please reload CAPTCHA.