現場で汗を流すエンジニア諸君、お疲れ様。
今日は、多くの開発者が「なんとなく」実装し、そして多くのインシデントの温床となっている「OpenID Connect (OIDC) のIDトークン検証」について、裏側の泥臭い現実を話そう。
「JWTのライブラリを使っているから大丈夫」なんて思っていたら、明日にでも君のバックエンドが踏み台にされるかもしれない。なぜなら、ライブラリのメソッドを呼ぶだけでは、セキュリティの半分も担保できていないからだ。
1. なぜ「署名検証」だけでは足りないのか
JWT(JSON Web Token)を扱う際、多くのエンジニアが iss(発行者)や aud(対象者)のチェックを怠る。しかし、最も恐ろしいのは「アルゴリズムの強制」を忘れることだ。
攻撃者は、公開鍵で検証すべき署名を、なんと「対称鍵(HS256)」として解釈させようとする。具体的には、JWTのヘッダーにある alg を none に書き換えたり、公開鍵のビット列をHMACの秘密鍵として流用させたりする攻撃だ。これが成功すれば、君のシステムは攻撃者が発行した偽のIDトークンを「本物」と誤認し、特権ユーザーとしてログインを許してしまう。
2. 実務で必須の検証チェックリスト
IDトークンを受け取った際、最低限以下のロジックを自前、あるいはライブラリの設定で強制しなければならない。
1. アルゴリズムの固定: alg ヘッダーを信用せず、期待するアルゴリズム(例: RS256 や ES256)のみを許可する。
2. 公開鍵の取得: jwks_uri から公開鍵を取得する際は、必ず信頼できる通信経路(HTTPS)を経由し、鍵のローテーションに対応する。
3. 発行者(iss)の検証: 自分の予期するIdP以外のトークンは即座に破棄する。
4. 対象者(aud)の検証: そのトークンが「自サービス宛」であることを厳密にチェックする。
5. 有効期限(exp)と現在時刻の差分: nbf(Not Before)や iat(Issued At)を含め、時計のズレを考慮した猶予時間(Clock Skew)を適切に設定する。
3. 実践:Python (PyJWT) による堅牢な検証コード
ライブラリを使うにしても、引数の渡し方で強度が変わる。以下は、攻撃者に付け入る隙を与えない、実戦的な実装例だ。
import jwt
import requests
from jwt import PyJWKClient
def verify_oidc_token(token, expected_aud, expected_iss):
# 1. JWKSエンドポイントから公開鍵を取得(キャッシュ戦略も重要だが今回は割愛)
url = "https://your-idp.com/.well-known/jwks.json"
jwks_client = PyJWKClient(url)
signing_key = jwks_client.get_signing_key_from_jwt(token)
try:
# 2. 検証の肝:algorithmsをリストで厳格に指定する
# これにより、攻撃者がalgヘッダーを書き換えても検証で弾かれる
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"], # ここでアルゴリズムを固定!
audience=expected_aud,
issuer=expected_iss,
options={
"verify_aud": True,
"verify_iss": True,
"require": ["exp", "iat", "iss", "aud"] # 必須クレームの強制
}
)
return payload
except jwt.exceptions.InvalidTokenError as e:
# ログには詳細を出すが、ユーザーには「認証失敗」とだけ返す(情報漏洩防止)
print(f"セキュリティ警告: 不正なトークンが検出されました: {e}")
return None
4. 運用上の盲点:JWKSのキャッシュとリプレイ攻撃
コードが書けても、インフラ側でコケるケースが多い。
- JWKSのキャッシュ:
jwks_uriをリクエストごとに叩くと、IdPに対してDoS攻撃を仕掛けることになり、自身のサービスも停止する。必ず公開鍵をメモリ上でキャッシュし、Cache-Controlヘッダーやkid(Key ID)に基づいて動的に更新する設計にすること。 - WAFでのブロック:
Authorization: Bearer <token>の値をWAFで検査し、長さが異常なものや、既知の攻撃パターン(SQLiやXSSのペイロードがJWT内に埋め込まれたもの)をフィルタリングするルールを適用しておこう。
最後に:セキュリティは「設定」で完成する
エンジニア諸君、セキュリティは「祈り」ではなく「検証可能な構造」だ。
今回示したような検証コードを単にコピーするのではなく、君たちの環境の jwks_uri や issuer を環境変数(.env 等)から読み込み、コード内にハードコーディングしない設計を徹底してほしい。
システムが巨大になればなるほど、認証基盤への攻撃は巧妙化する。だが、基本に立ち返り「入力値(トークン)はすべて悪意がある」という前提で検証を行えば、ほとんどの脆弱性は未然に防げる。
次にコードを書くときは、algorithms 引数が空になっていないか、もう一度確認してくれ。それが、プロのエンジニアの流儀だ。
コメント