Search code examples
node.jscachinggoogle-openid

How to maintain a cache of the public keys from Google's OpenID Connect discovery document


I am working on a Node.js server side validation of json web tokens received from cross origin ajax clients. Presumably the tokens are generated by Google OpenID Connect which states the following:

To use Google's OpenID Connect services, you should hard-code the Discovery-document URI into your application. Your application fetches the document, then retrieves endpoint URIs from it as needed.

You may be able to avoid an HTTP round-trip by caching the values from the Discovery document. Standard HTTP caching headers are used and should be respected.

source: https://developers.google.com/identity/protocols/OpenIDConnect#discovery

I wrote the following function that uses request.js to get the keys and moment.js to add some timestamp properties to a keyCache dictionary where I store the cached keys. This function is called when the server starts.

function cacheWellKnownKeys(uri) {
  var openid = 'https://accounts.google.com/.well-known/openid-configuration';

  // get the well known config from google
  request(openid, function(err, res, body) {
    var config               = JSON.parse(body);
    var jwks_uri             = config.jwks_uri;
    var timestamp            = moment();

    // get the public json web keys
    request(jwks_uri, function(err, res, body) {
      keyCache.keys          = JSON.parse(body).keys;
      keyCache.lastUpdate    = timestamp;
      keyCache.timeToLive    = timestamp.add(12, 'hours');
    });
  });
}

Having successfully cached the keys, my concern now is regarding how to effectively maintain the cache over time.

Since Google changes its public keys only infrequently (on the order of once per day), you can cache them and, in the vast majority of cases, perform local validation.

source: https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken

Since Google is changing their public keys every day, my idea with the timestamp and timeToLive properties of keyCache is to do one of two things:

  1. Set a timeout every 12 hours to update the cache
  2. Deal with the case where Google changes their public keys in between my 12 hour update cycle. The first failed token validation on my end triggers a refresh of the key cache followed by one last attempt to validate the token.

This seems like a viable working algorithm until I consider an onslaught of invalid token requests that result in repeated round trips to the well known config and public keys while trying to update the cache.

Maybe there's a better way that will result in less network overhead. This one line from the first quote above may have something to do with developing a more efficient solution but I'm not sure what to do about it: Standard HTTP caching headers are used and should be respected.

I guess my question is really just this...

Should I be leveraging the HTTP caching headers from Google's discovery document to develop a more efficient caching solution? How would that work?


Solution

  • The discovery document has property jwks_uri which is the web address of another document with public keys. This other document is the one Google is referring to when they say...

    Standard HTTP caching headers are used and should be respected.

    An HTTP HEAD request to this address https://www.googleapis.com/oauth2/v3/certs reveals the following header:

    HTTP/1.1 200 OK
    Expires: Wed, 25 Jan 2017 02:39:32 GMT
    Date: Tue, 24 Jan 2017 21:08:42 GMT
    Vary: Origin, X-Origin
    Content-Type: application/json; charset=UTF-8
    X-Content-Type-Options: nosniff
    x-frame-options: SAMEORIGIN
    x-xss-protection: 1; mode=block
    Content-Length: 1472
    Server: GSE
    Cache-Control: public, max-age=19850, must-revalidate, no-transform
    Age: 10770
    Alt-Svc: quic=":443"; ma=2592000; v="35,34"
    X-Firefox-Spdy: h2
    

    Programmatically access these header fields from the response object generated by request.js and parse the max-age value from it, something like this:

    var cacheControl = res.headers['cache-control'];      
    var values = cacheControl.split(',');
    var maxAge = parseInt(values[1].split('=')[1]);
    

    The maxAge value is measured in seconds. The idea then is to set a timeout based on the maxAge (times 1000 for millisecond conversion) and recursively refresh the cache upon every timeout completion. This solves the problem of refreshing the cache on every invalid authorization attempt, and you can drop the timestamp stuff you're doing with moment.js

    I propose the following function for handling the caching of these well known keys.

    var keyCache = {};
    
    /**
     * Caches Google's well known public keys
     */
    function cacheWellKnownKeys() {
        var wellKnown= 'https://accounts.google.com/.well-known/openid-configuration';
    
        // get the well known config from google
        request(wellKnown, function(err, res, body) {
            var config    = JSON.parse(body);
            var address   = config.jwks_uri;
    
            // get the public json web keys
            request(address, function(err, res, body) {
    
                keyCache.keys = JSON.parse(body).keys;
    
                // example cache-control header: 
                // public, max-age=24497, must-revalidate, no-transform
                var cacheControl = res.headers['cache-control'];      
                var values = cacheControl.split(',');
                var maxAge = parseInt(values[1].split('=')[1]);
    
                // update the key cache when the max age expires
                setTimeout(cacheWellKnownKeys, maxAge * 1000);    
            });
        });
    }