OAuth 2.0だけでは不十分?2026年の常識「PKCE」必須化と実装ガイド

「正しくOAuth 2.0を実装したはずなのに、ユーザーのアカウントが乗っ取られた」。これは悪夢のような話ですが、モバイルアプリやSPA(Single Page Application)において、古い認証フローを使い続けていると実際に起こりうる現実です。

PKCE(ピクシー / Proof Key for Code Exchange)とは、OAuth 2.0の認可プロセスにおいて、リクエストを行ったアプリとトークンを交換するアプリが同一であることを暗号学的に保証する拡張仕様です。 現在、OAuth 2.1ドラフトでは、すべてのクライアントに対してPKCEの利用が推奨(事実上の必須化)されています。

なぜ「認可コード」だけでは危険なのか(荷物預かり所の比喩)

Concept: 荷物預かり所の引換券
従来の認可コードフローは、「荷物(アクセストークン)」を預かり所から受け取るために「引換券(認可コード)」を使う仕組みに似ています。しかし、もし泥棒があなたの引換券を盗み見たらどうなるでしょうか?泥棒は引換券を持って窓口に行き、荷物を堂々と受け取れてしまいます。

PKCEは、これに「本人確認」を追加します。引換券を渡す際に、「預けた時に見せた身分証(Code Verifier)をもう一度見せてください」と要求するのです。これなら、引換券を盗まれても荷物は守られます。

技術的な文脈では、この「泥棒」は認可コード横取り攻撃(Authorization Code Interception Attack)と呼ばれます。特にカスタムスキーム(例: myapp://)を使用するモバイルアプリや、ブラウザ履歴に残るURLを扱うSPAでは、悪意あるアプリが認可コードを傍受し、アクセストークンを取得してしまうリスクがあります。

モダンなPKCE実装(Web Crypto API)

PKCEの核となるのは、code_verifier(ランダムな文字列)とcode_challenge(そのハッシュ値)の生成です。外部ライブラリに依存せず、ブラウザ標準のWeb Crypto APIを使用した実装例を紹介します。


// PKCEユーティリティ (TypeScript / Modern JavaScript)
// ターゲット: モダンブラウザ (SPA), React/Vue/Next.jsなど

/**
 * 1. Code Verifierの生成
 * ランダムな文字列 (43-128文字) を生成します。
 * 認可リクエストを開始する前に生成し、sessionStorage等に一時保存します。
 */
function generateCodeVerifier(length = 128): string {
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  const array = new Uint8Array(length);
  window.crypto.getRandomValues(array);
  
  return Array.from(array)
    .map(byte => charset[byte % charset.length])
    .join('');
}

/**
 * 2. Code Challengeの生成
 * VerifierをSHA-256でハッシュ化し、Base64URLエンコードします。
 * この値を認可リクエスト(GET /authorize)に含めます。
 */
async function generateCodeChallenge(verifier: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await window.crypto.subtle.digest('SHA-256', data);
  
  return base64UrlEncode(digest);
}

/**
 * ヘルパー: Base64URLエンコード
 * 標準のBase64とは異なり、'+' -> '-', '/' -> '_', '='削除 の処理が必要です。
 */
function base64UrlEncode(buffer: ArrayBuffer): string {
  const bytes = new Uint8Array(buffer);
  const len = bytes.byteLength;
  let binary = '';
  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return window.btoa(binary)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// --- 使用例 ---
(async () => {
  const verifier = generateCodeVerifier();
  const challenge = await generateCodeChallenge(verifier);

  console.log('Verifier (保存用):', verifier); 
  console.log('Challenge (送信):', challenge);
  
  // 1. 認可リクエスト
  // https://auth.example.com/authorize?response_type=code&client_id=...&code_challenge=${challenge}&code_challenge_method=S256
  
  // 2. トークン交換時 (POST /token)
  // body: code=...&code_verifier=${verifier}
})();

注意点 (Pitfalls):

  • S256を強制する: code_challenge_methodには必ずS256を指定してください。plainは後方互換性のためのものであり、セキュリティ強度が下がります。
  • Verifierの保存場所: code_verifierはトークン交換時に必要になるため、ブラウザのsessionStoragehttpOnlyクッキーに一時保存する必要があります。リダイレクト後に消えないように注意してください。

非推奨警告: Implicit Flow(インプリシットフロー)は絶対に使用しないでください。アクセストークンがURLフラグメントに含まれてしまい、履歴に残るなどの重大な脆弱性があります。OAuth 2.1では削除される予定です。

Frequently Asked Questions (FAQ)

Q. バックエンドがあるWebアプリ(Confidential Client)でもPKCEは必要ですか?

A. はい、強く推奨されます(OAuth 2.1では必須)。
以前は「Client Secret(秘密鍵)」を持てるサーバーサイドアプリには不要とされていました。しかし、認可コードインジェクション攻撃を防ぐ効果があるため、現在ではすべてのクライアントタイプでPKCEを利用するのがベストプラクティスです。

Q. PKCEを使えば state パラメータは不要になりますか?

A. 完全には不要になりませんが、CSRF対策としての役割はPKCEでカバー可能です。
ただし、stateパラメータは「ログイン前のユーザーの状態(どのページを見ていたか等)を復元する」ために依然として有用です。セキュリティ目的だけであればPKCEで十分なケースが多いですが、多くのIdP(Identity Provider)は依然としてstateを推奨しています。

Q. code_verifier の有効期限はありますか?

A. 明示的な仕様はありませんが、認可コード自体の有効期限が短く設定されています(通常10分以内)。
code_verifierは一度のトークン交換で使い捨てるべき値(ワンタイム)です。トークン取得に成功したら、保存していたVerifierは即座に削除してください。

Post a Comment