OAuth 2.0の「認可コード横取り」を許すな:PKCE実装こそが現代の防波堤だ
現場でコードをレビューしていると、いまだに「認可コード(Authorization Code)があれば認証できる」と勘違いしているエンジニアに出くわす。OAuth 2.0のフローを「ただのログイン機能」と捉えているなら、それは重大な認識の誤りだ。
特にモバイルアプリやSPA(Single Page Application)において、認可コードを盗み取る攻撃は今や教科書レベルの常套手段になっている。今日は、この脆弱性を根底から叩き潰す「PKCE(Proof Key for Code Exchange)」について、現場の知見を交えて深掘りしていく。
—
1. なぜ「認可コード」は盗まれるのか?(攻撃者の視点)
攻撃者が狙うのは、ブラウザとアプリの間でやり取りされる「認可コード」だ。
1. カスタムURLスキームの悪用: モバイルアプリがmyapp://callbackのようなスキームで認可コードを受け取っている場合、悪意のあるアプリが同じスキームを登録(ハイジャック)して、正当なアプリの前に割り込む。
2. ブラウザの履歴やログ: リダイレクトURLに認可コードが露出しているため、ブラウザの履歴やログからコードが漏洩するリスクがある。
攻撃者は、盗んだコードを自分のサーバーで「トークン要求」へとすり替え、ユーザーになりすましてアクセストークンを奪取する。これが「認可コード横取り攻撃」だ。
—
2. PKCEがもたらす「証明」の概念
PKCEは、この「横取り」を防ぐために、「認可コードを要求した本人」と「トークンを要求した本人」が同一人物であることを暗号学的に証明させる仕組みだ。
- Code Verifier: クライアントが生成するランダムな文字列(秘密の鍵)。
- Code Challenge: VerifierをSHA-256でハッシュ化したもの。
流れはシンプルだ。最初に「Code Challenge」を認可サーバーに預け、後から「Code Verifier」を提示することで「俺がさっきの本人だ」と証明する。途中でコードを盗まれても、攻撃者はVerifierを知らないため、トークン交換ができない。
—
3. 実践:PKCE実装サンプル(Node.js / JavaScript)
理論は分かっても、実装で詰まるのがエンジニアの性。以下に、OAuthクライアント側でVerifierを生成し、認可リクエストを送るまでの実務的なコードを示す。
const crypto = require(‘crypto’);
// 1. Code Verifierを生成(43〜128文字のランダム文字列)
const generateVerifier = () => {
return crypto.randomBytes(32).toString(‘base64url’);
};
// 2. Code Challengeを生成(VerifierをSHA-256ハッシュ化)
const generateChallenge = (verifier) => {
return crypto.createHash(‘sha256’).update(verifier).digest(‘base64url’);
};
// 実装例:認可リクエストのURL構築
const verifier = generateVerifier();
const challenge = generateChallenge(verifier);
// 後でトークン交換時に使うためにverifierをセッションストレージに保存
sessionStorage.setItem(‘pkce_verifier’, verifier);
const authUrl = https://auth.example.com/authorize? +
response_type=code& +
client_id=YOUR_CLIENT_ID& +
redirect_uri=YOUR_REDIRECT_URI& +
code_challenge=${challenge}& +
code_challenge_method=S256; // S256が鉄則。plainは絶対に使わないこと
console.log(“認可エンドポイントへリダイレクト:”, authUrl);
—
4. トークン交換時の検証(バックエンド/フロントエンド)
認可サーバーからリダイレクトされてきたら、保存しておいたpkce_verifierを添えてトークンを要求する。
// トークン要求時のPOSTリクエスト(例:axiosなど)
const exchangeToken = async (code) => {
const verifier = sessionStorage.getItem(‘pkce_verifier’);
const response = await axios.post(‘https://auth.example.com/token’, {
grant_type: ‘authorization_code’,
client_id: ‘YOUR_CLIENT_ID’,
code: code,
redirect_uri: ‘YOUR_REDIRECT_URI’,
code_verifier: verifier // ここで本人性を証明
});
return response.data;
};
—
5. セキュリティチーフからの「現場の教訓」
PKCEを実装する際、多くのエンジニアが陥る罠がある。これだけは必ず守ってほしい。
code_challenge_method=plainを避ける:
互換性のためにplain(ハッシュ化なし)が用意されていることがあるが、現代のセキュリティ基準では論外だ。必ずS256を強制すること。
- 認可サーバーの設定:
サーバー側で「PKCE必須」をオンにできるなら、即座に有効化してほしい。Auth0やOkta、AWS CognitoなどのIdPを使っているなら、設定画面に必ずチェックボックスがあるはずだ。
- 通信経路の保護:
PKCEは認可コードを保護するが、TLS(HTTPS)の不備を補完するものではない。証明書の管理、HSTSの設定は前提条件であることを忘れないでほしい。
まとめ
「動けばいい」という甘えは、インシデントという形で必ず自分たちに跳ね返ってくる。OAuth 2.0の仕様は複雑だが、PKCEはクライアント側の実装だけで防げる非常に費用対効果の高いセキュリティ対策だ。
明日からの開発では、PKCEの有無をコードレビューの必須チェック項目に加えてほしい。それが、自分たちのプロダクトとユーザーを守るための、プロのエンジニアの流儀だ。
コメント