OAuth 2.0の「認可コード横取り」はなぜ防げないのか?PKCE強制化がエンジニアの必須教養である理由
現場でコードをレビューしていると、いまだに「OAuth 2.0の認可フローなら安全だよね」という甘い認識に遭遇することがある。断言しよう。PKCE(Proof Key for Code Exchange)なしの認可コードフローは、現代のWeb環境では「鍵のかかっていない玄関」に等しい。
今日は、なぜ攻撃者が認可コードを狙うのか、そして私たちが実装で何をすべきか、インシデントの最前線から解説する。
—
1. なぜ「認可コード」が狙われるのか?
OAuth 2.0の認可コードフローでは、ブラウザを介して code がリダイレクトされる。このとき、攻撃者は以下の手段で code を横取りしようと試みる。
- カスタムURLスキームのハイジャック: モバイルアプリやデスクトップアプリが特定のスキーム(例:
myapp://)をリッスンしている場合、攻撃者が同じスキームを登録して横取りする。 - ブラウザの履歴やログ: 認可コードがブラウザの履歴やプロキシサーバーのログに残ることで、攻撃者がそれを回収する。
本来、サーバーサイドのクライアント(機密情報を持つバックエンド)であれば client_secret で身元を証明できるが、SPA(シングルページアプリケーション)やモバイルアプリといった「公開クライアント」には秘密を隠し持てない。ここで登場するのが PKCE だ。
—
2. PKCEの本質:動的な「ワンタイム鍵」
PKCEは、認可リクエスト時に「自分だけが知っている秘密(Code Verifier)」をハッシュ化し、その「ハッシュ値(Code Challenge)」をあらかじめ送る。認可コードを受け取った後、改めて「元の秘密」を提示させることで、横取りした攻撃者がコードを使えないようにする仕組みだ。
攻撃シナリオ(PoCイメージ)
1. 正当なアプリ: code_challenge を送って認可を要求。
2. 攻撃者: 中間者攻撃でリダイレクトから code を盗む。
3. 攻撃者: 盗んだ code でトークンを要求するが、code_verifier を持っていないため、認可サーバーに拒否される。
—
3. 実装のベストプラクティス(Node.js/JavaScript)
現代のWeb開発では、oidc-client-ts や msal などのライブラリを使うのが定石だが、仕組みを理解するために「PKCEの核」となる部分を実装しよう。
// PKCEのCode VerifierとChallengeを生成する関数
async function generatePKCE() {
// 1. ランダムな文字列 (Code Verifier) を生成
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
const verifier = btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, ‘-‘).replace(/\//g, ‘_’).replace(/=+$/, ”);
// 2. SHA-256でハッシュ化し、Code Challengeを作成
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await window.crypto.subtle.digest(‘SHA-256’, data);
const challenge = btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
.replace(/\+/g, ‘-‘).replace(/\//g, ‘_’).replace(/=+$/, ”);
return { verifier, challenge };
}
// 認可リクエスト送信時(簡易例)
const { verifier, challenge } = await generatePKCE();
// ローカルストレージにVerifierを保存(トークン交換時に使用)
localStorage.setItem(‘pkce_verifier’, verifier);
// 認可サーバーへのURLに challenge と method を含める
// ?code_challenge=…&code_challenge_method=S256
トークン交換の際は、保存しておいた pkce_verifier を送信するだけでいい。これだけで、認可コードは「あなた」しか使えない強力なパスポートに昇格する。
—
4. インフラ・セキュリティ担当者へのアドバイス
アプリ開発者だけでなく、インフラ側も「入り口」で防御を固める必要がある。特にWAF(Web Application Firewall)の設定は重要だ。
Nginx/WAFでの推奨設定
認可コードを含むリクエストに対し、不審なリダイレクト先や、PKCEパラメータを欠いたリクエストを検知する設定を検討してほしい。
Nginxでのリクエストフィルタリング例
location /callback {
# 認可コードの長さや形式を検証(異常に短い/長いものは拒否)
if ($arg_code = “”) {
return 403; # コードがないリクエストは即座に遮断
}
# 認可コードがURLパラメータで漏洩するのを防ぐため、
# 認可サーバー側の設定で State パラメータを必須化し、CSRF対策も徹底する
}
—
まとめ:セキュリティは「性悪説」で設計せよ
「PKCEを実装するのは面倒だ」という現場の声を聞くことがある。しかし、ユーザーのアクセストークンを奪われることの代償は、実装コストの数千倍だ。
1. クライアントタイプに関わらずPKCEを強制する: 最近のOAuth 2.1ドラフトでは、PKCEがデフォルトになっている。
2. 認可サーバー側で検証を厳格化する: code_challenge_method=S256 以外は受け付けないポリシーを徹底すること。
セキュリティに「過剰」はない。今日から君たちのコードベースにPKCEが組み込まれているか、再確認してほしい。それが、プロのエンジニアとしての最低限の責任だ。
もし実装で詰まったら、いつでも相談してくれ。バグを見つけることより、バグを生まない設計をすることが、我々シニアエンジニアの真の仕事なのだから。
コメント