4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

json-jwt gem の JWK Set キャッシュ戦略

Last updated at Posted at 2022-08-12

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 をキャッシュしておくといった利用方法は取れないことになる。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?