如何从Google的OpenID Connect发现文档维护公钥的缓存

时间:2017-01-24 15:28:52

标签: node.js caching google-openid

我正在研究从交叉源ajax客户端收到的json web令牌的Node.js服务器端验证。据推测,令牌由Google OpenID Connect生成,其中包含以下内容:

  

要使用Google的OpenID Connect服务,您应该将Discovery-document URI硬编码到您的应用程序中。您的应用程序提取文档,然后根据需要从中检索端点URI。

     

您可以通过缓存Discovery文档中的值来避免HTTP往返。使用标准HTTP缓存标头,应予以遵守。

     

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

我编写了以下函数,该函数使用request.js来获取键,并使用moment.js将一些时间戳属性添加到我存储缓存键的keyCache字典中。服务器启动时会调用此函数。

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');
    });
  });
}

成功缓存了密钥之后,我现在关心的是如何有效地维护缓存。

  

由于Google很少更改其公钥(每天一次),您可以缓存它们,并在绝大多数情况下执行本地验证。

     

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

由于Google每天都在更改公钥,因此我对timestamptimeToLive keyCache属性的想法是做两件事之一:

  1. 每12小时设置一次超时以更新缓存
  2. 处理Google在12小时更新周期之间更改公钥的情况。我端的第一个失败的令牌验证会触发刷新密钥缓存,然后最后一次尝试验证令牌。
  3. 这似乎是一个可行的工作算法,直到我考虑无效令牌请求的冲击导致在尝试更新缓存时重复往返于众所周知的配置和公钥。

    也许有更好的方法可以减少网络开销。上面第一个引用中的这一行可能与开发更有效的解决方案有关,但我不知道该怎么做:Standard HTTP caching headers are used and should be respected.

    我想我的问题实际上就是这个......

    我是否应该利用Google发现文档中的HTTP缓存标头来开发更有效的缓存解决方案?这会怎么样?

1 个答案:

答案 0 :(得分:1)

discovery document具有属性jwks_uri,该属性是具有公钥的另一个文档的网址。这个其他文件是谷歌在他们说...时所指的那个......

  

使用标准HTTP缓存标头,应予以遵守。

对此地址https://www.googleapis.com/oauth2/v3/certs的HTTP HEAD请求会显示以下标头:

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

以编程方式从request.js生成的响应对象中访问这些头字段,并从中解析max-age值,如下所示:

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

maxAge值以秒为单位。接下来的想法是根据maxAge设置超时(毫秒转换的时间为1000),并在每次超时完成时递归刷新缓存。这解决了在每次无效授权尝试时刷新缓存的问题,您可以使用moment.js删除正在执行的时间戳。

我建议使用以下函数来处理这些众所周知的密钥的缓存。

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);    
        });
    });
}