JWK Set 取得タイミング
- OIDC ID Tokenの署名検証時
- その他 JWT 署名検証時
- e.g.,) Apple の Server-to-server Notification
JWK Set 取得頻度とキャッシング
JWK Set には複数の鍵が含まれており、固定の URL から取得可能になっていることが多い。
OpenID Connect では /.well-known/openid-configuration
にアクセスすることで JWK Set が公開されている URL (jwks_uri
) を取得できる。
このエンドポイントから返される鍵は、一定期間ごとに新しいものに差し替えられたりするが、jwks_uri
は固定であるため署名検証者は常にこのエンドポイントにアクセスすれば署名検証に必要な鍵が得られるようになっている。
しかし、ID Token や Server-to-server Notification が届くたびにこのエンドポイントにアクセスするのは負荷が高いので、一定期間この JWK Set をキャッシュしたくなる。
一方で、一般的に OpenID Connect では、鍵の差し替え (Key Rotation) は IdP が任意のタイミングで行い、その際事前に RP に通知することもない。
よって RP 側はキャッシュした JWK Set に署名検証に必要な鍵が含まれていないという事象が任意のタイミングで発生することになる。
JWK Set キャッシュと JWT の kid ヘッダー
JWK Set を jwks_uri
で公開し、ID Token やその他 JWT をその JWK Set 中の特定の鍵で署名検証させる場合、JWK Set 中の各鍵に kid
を付与しつつ、署名検証対象となる ID Token や JWT のヘッダーに署名に使うべき鍵の kid
を付与することが一般的である。
そのため、キャッシュされた JWK Set に署名対象の ID Token / JWT に付与された kid
を持つ鍵が存在しなければ、キャッシュを破棄して新たに jwks_uri
から JWK Set を取得し直すべきである。
こんな感じ?
cached_jwks = Cache.read('apple:jwks')
jwk = cached_jwks.detect { |jwk| jwk.kid == kid }
if jwk.present?
jwk
else
new_jwks = fetch(jwks_uri).detect { |jwk| jwk.kid == kid }
Cache.write('apple:jwks', new_jwks)
end
ただ、こういうコードをライブラリ中で扱おうとする少しめんどくさいので、json-jwt
では kid
を Cache Key に含めて JWK Set をキャッシュするという方法を取っている。
こんな感じ。(実際のコードはこちら、詳しい使い方はこちら)
def self.fetch(jwks_uri, kid:, auto_detect: true)
cache_key = [
'json:jwk:set',
OpenSSL::Digest::MD5.hexdigest(jwks_uri),
kid
].collect(&:to_s).join(':')
jwks = Set.new(
JSON.parse(
cache.fetch(cache_key) do
http_client.get_content(jwks_uri)
end
)
)
if auto_detect
jwks[kid] or raise KidNotFound
else
jwks
end
end
この方法では、JWK Set に複数の鍵が含まれている場合、kid
単位で別々に同じコンテンツがキャッシュされうるため、キャッシュコンテンツは増えてしまうが、一般的には JWS Set に1,000個や10,000個もの JWK が含まれるような実装は存在しないので、せいぜい5~6個の同じコンテンツが重複してキャッシュされてしまうにすぎないだろうということで、json-jwt
では実装のシンプルさを優先している。
なお、上記コードではデフォルトでは JWK Set 取得と同時に kid
による JWK の特定まで行なっている。
JWK::Set
クラスが fetch
すると JWK
インスタンスを返してくるのは少し違和感はあるのだが、kid
を指定して fetch
しておいて、JWK Set 全体が欲しいケースなどそうそうないだろうという判断である。
JWK Set をキャッシュする際の注意点
Cache Key に kid
を利用する都合上、kid
が特定されるまえに (= ID Token 等を Parse して kid
ヘッダーを見る前に) JWK Set を取得するといった処理はできない。
よって、ID Token 等を受け取る前に事前に JWK Set をキャッシュしておくといった利用方法は取れないことになる。