如何维护来自 Google 的 OpenID Connect 发现文档的 public 密钥的缓存

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

我正在对从跨源 ajax 客户端收到的 json 网络令牌进行 Node.js 服务器端验证。据推测,令牌是由 Google OpenID Connect 生成的,其中说明如下:

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

我编写了以下函数,它使用 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');
    });
  });
}

成功缓存密钥后,我现在关心的是如何随着时间的推移有效地维护缓存。

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

由于 Google 每天都在更改他们的 public 密钥,我对 keyCachetimestamptimeToLive 属性的想法是执行以下操作之一两件事:

  1. 每 12 小时设置一次超时以更新缓存
  2. 处理 Google 在我的 12 小时更新周期之间更改其 public 密钥的情况。我这边第一次失败的令牌验证会触发密钥缓存的刷新,然后是最后一次验证令牌的尝试。

这似乎是一个可行的工作算法,直到我考虑到无效令牌请求的冲击导致在尝试更新缓存时重复往返于众所周知的配置和 public 密钥。

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

我想我的问题真的只是这个...

我是否应该利用 Google 的发现文档中的 HTTP 缓存 headers 来开发更高效的缓存解决方案?那将如何运作?

discovery document 有 属性 jwks_uri 是另一个文档的网址 public 键。当他们说...

时,Google 指的是另一份文件

Standard HTTP caching headers are used and should be respected.

对此地址 https://www.googleapis.com/oauth2/v3/certs 的 HTTP HEAD 请求显示以下 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

以编程方式从 request.js 生成的响应 object 中访问这些 header 字段并从中解析 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);    
        });
    });
}