【実務・中級編】 OpenID Connect (OIDC) IDトークンの署名検証と発行者(iss)・オーディエンス(aud)検証 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

現場のエンジニア諸君、お疲れ様。
今日は、多くの開発者が「なんとなく動いているから」と放置しがちな、IDトークン(JWT)の検証における「致命的な穴」について話そう。

現場でインシデント対応をしていると、このOIDC(OpenID Connect)の実装ミスが原因で、認証がスルーされ、バックエンドが丸裸にされているケースに何度も遭遇する。特に「署名アルゴリズムの固定」と「クレーム検証の甘さ」は、攻撃者にとっての格好の入り口だ。

教科書的な話は一旦置いて、「なぜ攻撃者はそこを突くのか」「どう実装すれば防げるのか」という、泥臭い実務の話をする。

—

1. 攻撃者が狙う「署名アルゴリズムの盲点」

JWTのヘッダーには alg(アルゴリズム)というフィールドがある。多くのライブラリで「受け取ったトークンの alg を信用して検証する」という実装をしてしまうのが、最初にして最大の過ちだ。

攻撃手法:alg: none 攻撃

もしバックエンドが「alg フィールドの値を見て、そのアルゴリズムで検証する」という実装をしていたら、攻撃者はこうする。

1. トークンのヘッダーを {"alg": "none"} に書き換える。
2. ペイロードを自分の都合のいいもの(sub: admin など)に書き換える。
3. 署名部分(JWTの3番目のセグメント)を空にする。

これだけで、脆弱な検証ライブラリは「署名なしでOK」と判断して通してしまう。これは極端な例だが、同様に RS256 を期待している場所で HS256 にすり替え、公開鍵を共有鍵として扱わせることで署名を偽造する攻撃(Key Confusion Attack)も定番だ。

—

2. 署名検証とクレーム検証の実装(Python例)

「ライブラリがよしなにやってくれる」を信じるな。最低限、「アルゴリズムの強制指定」と「iss(発行者)/aud(オーディエンス)の厳格な検証」は自分たちの手で制御する。

以下は、Pythonの PyJWT を使った、現場で使うべき堅牢な検証実装だ。

import jwt
from jwt import PyJWKClient

# 1. JWKSエンドポイントから公開鍵を取得(都度ハードコーディングは厳禁)
url = "https://auth.example.com/.well-known/jwks.json"
jwks_client = PyJWKClient(url)

def verify_id_token(token):
    try:
        # 2. 公開鍵をJWKSから取得
        signing_key = jwks_client.get_signing_key_from_jwt(token)
        
        # 3. ここが重要:検証アルゴリズムを強制的に固定(RS256以外は受け付けない)
        # 4. audとissを必ず検証する
        payload = jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            audience="my-client-id",
            issuer="https://auth.example.com"
        )
        return payload
    except jwt.ExpiredSignatureError:
        print("トークンの有効期限切れ")
    except jwt.InvalidTokenError as e:
        print(f"検証失敗: {e}")
    return None

なぜこの実装が重要なのか?

  • algorithms=["RS256"]: これを明示することで、攻撃者が alg: none や HS256 を指定しても、ライブラリ側で弾くようになる。
  • audience: これがないと、他のサービス向けに発行された正当なトークンを使い回す「なりすまし」を防げない。
  • issuer: これがないと、攻撃者が自分で立てた偽のOIDCサーバーから発行したトークンを受け入れてしまう。

—

3. なぜ「iss」と「aud」の検証をサボってはいけないのか

「うちのアプリはクローズドだから大丈夫」という甘い考えは捨ててくれ。

  • aud(オーディエンス)検証の欠如: 攻撃者が、同じ認証基盤を使っている「別サービスA」で正当なユーザーとしてログインし、そこで発行されたトークンを「サービスB(君たちのアプリ)」に流し込む。検証がなければ、君たちのサービスは「あ、これ認証済みユーザーだ」と判断し、ログインを許可してしまう。これが「クロスサービス攻撃」だ。
  • iss(発行者)検証の欠如: 認証基盤が複数ある環境や、将来的に連携先が増えることを想定すると、必ず発行者のURLをホワイトリストでチェックしなければならない。

—

4. 運用エンジニアへの提言:設定の守り方

コードだけでなく、インフラ側でも防御を固めるのがプロだ。

Nginxでのリクエスト制限(概念)

IDトークンが偽造されたリクエストが大量に飛んでくる場合、JWTの構造を解く前に、怪しいリクエストをWAFで落とす設定も有効だ。

# Nginx設定: 特定のパスへのリクエストでヘッダーにJWTが含まれていない場合、
# あるいはJWTの構造が明らかに異常な場合は403を返すなど
location /api/ {
    if ($http_authorization !~* "^Bearer [a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$") {
        return 403;
    }
    proxy_pass http://backend_app;
}

—

まとめ:信頼は「疑うこと」から始まる

セキュリティエンジニアの仕事は、「システムが受け取るデータは、常に攻撃者によって改ざんされている可能性がある」という前提で設計することだ。

1. 署名アルゴリズムは常にホワイトリストで固定する(ライブラリのデフォルトを鵜呑みにしない)。
2. iss, aud, exp(有効期限)の検証は必須項目。これらを省略したコードは、セキュリティ上「未完成」であるとみなせ。
3. 公開鍵はJWKSから動的に取得する。鍵のローテーションに対応できない設計は、長期的には必ず破綻する。

もし君のチームで、IDトークンの検証コードが jwt.decode(token, verify=False) のような、検証をオフにするオプションを含んでいるのを見つけたら……即座に修正のプルリクエストを投げてくれ。それが、君のシステムを守る一番の近道だ。

コードは嘘をつかない。だが、設定の甘さは嘘をつき続けて、ある日突然、君たちのシステムの脆弱性として牙を剥くぞ。常に疑って、厳格に実装しろ。期待している。

コメント

タイトルとURLをコピーしました