IDトークン検証は「性善説」で書くな。OIDC実装で死なないための鉄則
現場でコードレビューをしていると、未だに「とりあえずJWTをデコードして、中身のユーザーIDだけ見てログインさせる」という、セキュリティの地雷を踏み抜くような実装を見かける。
正直に言おう。OpenID Connect (OIDC) のIDトークンは、サーバーサイドで「全項目」を厳密に検証しなければ、それはただの「偽造可能な平文」と同じだ。
今回は、OWASP Top 10の「不適切な認証」を完全に封じ込めるための、IDトークン検証の勘所と実装のベストプラクティスを叩き込む。
—
なぜ「署名検証だけ」では不十分なのか?
多くの初心者は `jwt.verify(token, secret)` だけで満足する。だが、攻撃者はその先を狙う。
もしあなたが `iss`(発行者)や `aud`(対象読者)を確認していなかったら、攻撃者は自ら取得した「自分の正しいトークン」を、「あなたのサービスにログインさせたい別のサービス」に投げ込むことができる(Confused Deputy問題)。また、`exp`(有効期限)の検証をサボれば、盗み取られたトークンは永久に再利用される。
攻撃シナリオ:IDトークンのインジェクション
1. 攻撃者が、攻撃者自身のGoogleアカウントでログインし、IDトークンを取得。
2. 攻撃者はそのトークンを、被害者が利用するアプリの認証エンドポイントに送信。
3. アプリ側が `aud`(自分宛か)をチェックしていなければ、「Googleが発行した有効なトークンだ」と誤認し、攻撃者をログインさせてしまう。
—
堅牢な検証のチェックリスト
IDトークンを受け取った際、ライブラリに任せきりにせず、以下の項目を必ずチェックする実装を強制してほしい。
- 署名の検証: IDプロバイダ(IdP)の公開鍵(JWKS)で確実に署名を検証する。
- iss (Issuer) の検証: トークンが信頼するIdPから発行されたものか。
- aud (Audience) の検証: トークンが「あなたのアプリ」のために発行されたものか。
- exp (Expiration) の検証: 現在時刻が有効期限内か。
- nonce の検証: (認証フロー時)リクエスト時に送った値と一致するか(リプレイ攻撃対策)。
—
【実務コード】Pythonによるセキュアな検証実装
`PyJWT` を使った、実務レベルの検証ロジックだ。このままテンプレートとして使ってくれ。
import jwt
from jwt import PyJWKClient
IdPが公開しているJWKSエンドポイント
jwks_client = PyJWKClient(“https://accounts.google.com/.well-known/openid-configuration”)
def verify_id_token(token, expected_nonce):
try:
# 1. トークンのヘッダーから鍵ID(kid)を取得し、公開鍵をフェッチする
signing_key = jwks_client.get_signing_key_from_jwt(token)
# 2. 検証設定(ここで厳密にチェックする)
payload = jwt.decode(
token,
signing_key.key,
algorithms=[“RS256″],
audience=”YOUR_CLIENT_ID”, # ここをサボるな
issuer=”https://accounts.google.com”,
options={“require”: [“exp”, “iss”, “aud”, “nonce”]}
)
# 3. nonceの検証(リプレイ攻撃対策)
if payload.get(“nonce”) != expected_nonce:
raise jwt.InvalidTokenError(“Nonce mismatch”)
return payload
except jwt.ExpiredSignatureError:
print(“トークンが期限切れです”)
except jwt.InvalidTokenError as e:
print(f”検証失敗: {e}”)
return None
—
運用上の盲点:JWKSのキャッシュ戦略
ここで一つ、現場でよくある失敗を指摘しておく。「毎回JWKSエンドポイントへHTTPSリクエストを送る」実装はNGだ。 攻撃者があなたのサーバーからIdPに対して大量の外部リクエストを誘発させ、可用性を下げる(DoS)可能性がある。
- 解決策: JWKSはメモリ上にキャッシュせよ。`kid`(Key ID)をキーにして、公開鍵を一定時間(例:24時間)保持するロジックを必ず入れること。
—
インフラ層での防御(Nginx/WAF)
アプリケーションコードの修正が追いつかない場合の「最後の砦」として、WAFでのフィルタリングを検討せよ。
例えば、IDトークンがHTTPヘッダーの `Authorization: Bearer
ただし、「複雑な認証ロジックをWAFに書くな」というのが私の教訓だ。WAFはあくまで「異常なリクエストを弾く」フィルターであり、論理的な検証は必ずバックエンドのセキュアなコードで行うこと。
—
最後に:コードは嘘をつかない
セキュリティを「設定」だけで解決しようとせず、必ず「検証ロジック」をコードに刻み込むこと。
今回紹介した `aud` や `nonce` の検証を省略しただけで、あなたのサービスは一瞬で攻撃の踏み台に変わる。
「動くコード」を書くことは、エンジニアとして最低限の仕事だ。だが、「攻撃者の思考を先回りした、防御的なコード」を書くことこそが、我々エンジニアがプロとして誇るべき技術だ。
さあ、今すぐ自分のコードの検証設定を見直してくれ。もし「issuerの検証を忘れていた」なら、今日がそれを直す最高のタイミングだ。

コメント