JWTをサーバに問い合わせずに検証する
一言でいうと
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回ずつ比較します。