TT Lab
はじめる
学ぶ 学習パス コース

Keycloakと企業認証

JWTをサーバに問い合わせずに検証する

TT Labで続きを見る

一言でいうと

JWTの検証は、認証サーバーに問い合わせるのではなく、公開鍵で署名を確認してクレームを検査するローカルの演算です。だから速いのです。

なぜ必要なのか

トークンが有効かどうかを確認する方法は2つあります。認証サーバーのintrospectionエンドポイントに問い合わせるか、トークン自体の署名を検証するかです。

前者は正確です。たった今取り消されたトークンも、すぐに無効と判定されます。その代わり、リクエストのたびにネットワークの往復が発生します。マイクロサービス10個がそれぞれ問い合わせると、認証サーバーがボトルネックになります。

後者は、ネットワークなしで完結します。公開鍵さえあればよく、公開鍵はキャッシュできます。その代わり、取り消しを即座に反映できません。トークンの有効期限が切れるまでは、有効なものとして扱われます。

実務の答えは決まっています。アクセストークンを短く(5–15分)して、ローカル検証を使います。取り消しの反映遅延を、その寿命の長さまでに抑えるのです。リフレッシュトークンのローテーションを併用すれば、拒否リストなしでも実質的な無効化の効果が得られます。

どう動くのか

JWTは、ドットで区切られた3つの部分からなります。ヘッダー、ペイロード、署名です。最初の2つはBase64URLでエンコードされただけで、暗号化ではありません。誰でも読めます。そのため、JWTに秘密を入れてはいけません。

検証の順序は次のとおりです。

まず、ヘッダーのalgとkidを見ます。algがnoneであったり、想定と違っていたりしたら、ただちに拒否します。これを確認しなかったために署名の回避が起きた事故が、実際にあります。

kidを使って、JWKSエンドポイントから該当する公開鍵を探します。JWKSはキャッシュしますが、kidが見つからなければ1回更新します。鍵のローテーションのときに必要になります。

署名を検証します。RS256なら、公開鍵でRSA検証を行います。

次にクレームを見ます。exp(有効期限)、iss(発行者が私たちの知っているそのサーバーか)、aud(このトークンが私たちのAPI向けのものか)、そして必要ならnbfとazpです。

audの検査を忘れるミスが、特によくあります。同じ認証サーバーが発行した別のAPI向けのトークンが、私たちのAPIで通ってしまいます。

IDトークンとアクセストークンを混同することもよくあります。IDトークンは、クライアントアプリが「ユーザーが誰か」を確認するためのもので、audはクライアントです。API呼び出しには、アクセストークンを使わなければなりません。

現場での姿

鍵のローテーションがあると、JWKSには複数の鍵が同時に存在します。kidで選ぶ理由です。キャッシュのTTLを長くしすぎると、ローテーションの直後にすべての検証が失敗し、短すぎると認証サーバーにリクエストが集中します。たいていは数時間キャッシュし、kidのミスのときはすぐに更新する方式を取ります。

検証で必ず確認する5つのこと

署名さえ合っていればよいわけではありません。5つすべてを確認する必要があります。

クレーム 確認すること 見ないとどうなるか
iss 私たちが知っている発行者か 他の場所が発行したトークンが通る
aud このサービスを対象にしているか 他のサービス向けのトークンが通る
exp 期限切れではないか 永遠に有効になる
nbf まだ有効期間の前ではないか 未来のトークンが通る
署名 発行者の鍵で署名されているか 偽造トークンが通る

audを忘れることが最も多いミスです。同じ認証サーバーを使う別のアプリケーションの アクセストークンで、私たちのAPIを呼べるようになってしまいます。マイクロサービスが複数あるなら、 必ず確認します。

アルゴリズム混同攻撃

アルゴリズムをライブラリに任せてはいけません。トークンのヘッダーのalgをそのまま信じた瞬間に、 攻撃者がそれをnoneやHS256に書き換えて送ってきます。

정상: {"alg": "RS256", "kid": "abc"}   ← 공개키로 검증
공격: {"alg": "none"}                   ← 서명 없이 통과시키려는 시도
공격: {"alg": "HS256"}                  ← 공개키를 HMAC 비밀키로 쓰게 유도

2つ目が特に巧妙です。RS256の公開鍵は誰もが知っている値ですが、それをHMACの 共通鍵として使うと、攻撃者が有効な署名を作れてしまいます。

# ❌ 헤더를 믿는다
jwt.decode(token, key)

# ✅ 우리가 알고리즘을 정한다
jwt.decode(token, key, algorithms=["RS256"], audience="labhub-api",
           issuer="https://auth.labhub/realms/labhub")

鍵のローテーションとJWKSのキャッシュ

認証サーバーは、鍵を定期的に入れ替えます。アプリケーションはkid(鍵の識別子)で適切な 公開鍵を選び、知らないkidに出会ったらJWKSを再取得します。

1. 토큰 헤더의 kid 를 본다
2. 캐시에 있으면 그 키로 검증
3. 없으면 JWKS 엔드포인트를 다시 받는다 (여기에 속도 제한을 건다)

手順3に制限がないと、知らないkidを含むリクエストを大量に送りつけて、認証サーバーを ダウンさせられます。1分あたり数回に制限し、その間はキャッシュされた鍵だけを使います。

キャッシュの寿命は短く(5–15分)しますが、ローテーション中は古い鍵と新しい鍵が両方とも有効で なければなりません。認証サーバーが2つの鍵を同時に公開する期間を設ける理由です。

次のラボですること

Keycloakからトークンを受け取って3つの部分に分解し、クレームを確認し、JWKSから鍵を探して署名を検証し、ペイロードを改ざんして検証が失敗するところまで確認します。最後に、introspectionとローカル検証の所要時間を100回ずつ比較します。