PKCE

OAuth 2.0 の認可コードフローを、クライアントシークレットなしで安全に実行するための拡張

認証

PKCE とは

PKCE (Proof Key for Code Exchange、ピクシーと読む) は、RFC 7636 で定義された OAuth 2.0 の拡張で、認可コードの横取り攻撃を防ぐ仕組みである。元々はモバイルアプリや SPA など、クライアントシークレットを安全に保持できないパブリッククライアント向けに設計された。その後、2025 年 1 月に発行された RFC 9700 (BCP 240) が、パブリッククライアントには PKCE の使用を必須、機密クライアントには推奨、認可サーバーには PKCE のサポートを必須と定めたため、どの構成でも前提として扱う拡張になった。「OAuth 2.1 で全クライアント必須」と説明されることがあるが、OAuth 2.1 は 2026 年 8 月時点でまだドラフトであり、根拠として挙げるべきは RFC 9700 である。

なぜ PKCE が必要か

通常の認可コードフローでは、認可コードを横取りした攻撃者がトークンを取得できる。

1. ユーザーが認可サーバーにログイン
2. 認可サーバーがリダイレクト URL に認可コードを付与
3. 攻撃者がリダイレクトを傍受し、認可コードを横取り (ここを突かれる)
4. 攻撃者が認可コードでトークンを取得

PKCE は、認可コードとトークン交換を暗号学的に紐づけることで、横取りされた認可コードを無効化する。

PKCE のフロー

仕組みの要点は、値の通る経路を分けることにある。ブラウザのリダイレクトを経由する (フロントチャネル) のはハッシュ値の code_challenge だけで、元の秘密である code_verifier はクライアントとトークンエンドポイントの直接通信 (バックチャネル) にしか現れない。リダイレクトを覗ける位置にいる攻撃者はハッシュしか手に入らず、そこから元の値は逆算できない。

1. クライアントがランダムな code_verifier を生成 (43 文字以上 128 文字以下)
2. code_verifier を SHA-256 でハッシュし、base64url で符号化して code_challenge を作る
3. 認可リクエストに code_challenge を含める
4. 認可サーバーがリダイレクトで認可コードを返す
5. トークンリクエストに code_verifier を含める
6. 認可サーバーが code_verifier のハッシュと code_challenge を照合
7. 一致すればトークンを発行

攻撃者が認可コードを横取りしても、code_verifier を知らないためトークンを取得できない。横取りしたコードを利用者のセッションに紛れ込ませる認可コード注入も、コードと verifier の対応が合わないため成立しない。

変換方式は code_challenge_method で指定する。RFC 7636 が定義するのは plain (verifier をそのまま送る) と S256 (上記のハッシュと符号化) の 2 つだが、plain は経路上でそのまま盗める値を送るだけなので、実装は S256 一択と考えてよい。

防御が成り立つ条件は認可サーバー側にもある。code_challenge の有無で PKCE の適用が切り替わる実装では、攻撃者が認可リクエストからそのパラメータを落とすだけで検証を素通りできてしまう (PKCE ダウングレード攻撃)。RFC 9700 は、認可リクエストに code_challenge が無かった場合は code_verifier 付きのトークンリクエストを受け付けないことを認可サーバーの要件としている。クライアント側を正しく書いただけでは閉じない穴である。

実装

必要な処理そのものは数行で済む。randomBytes(32) を base64url にすると 43 文字になり、RFC 7636 の下限をちょうど満たす。注意点は verifier の置き場所で、認可リクエストからトークンリクエストまでリダイレクトを挟んで生き延びる必要がある。SPA なら sessionStorage、モバイルアプリなら OS の安全な保管領域を使う。

import { randomBytes, createHash } from 'crypto';

// 1. code_verifier を生成 (43〜128 文字のランダム文字列)
const codeVerifier = randomBytes(32).toString('base64url');

// 2. code_challenge を計算
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');

// 3. 認可リクエスト
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// 5. トークンリクエスト (認可コード + code_verifier)
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: codeVerifier,  // ← これが PKCE の核心
  }),
});

Cognito での PKCE

Cognito User Pool は PKCE をネイティブにサポートする。/oauth2/authorizecode_challengecode_challenge_method を付け、/oauth2/tokencode_verifier を渡すだけで、ユーザープール側の追加設定は要らない。ただし code_challenge_method に指定できるのは S256 だけで、plain は受け付けない。

呼称は移り変わっている。2026 年 8 月時点のログイン画面は managed login が既定で、従来の画面は classic hosted UI として併存している。両者の違いは見た目だけで、/passkeys/add を除けばエンドポイントのパスとパラメータは共通なので、Hosted UI 前提で書かれた古い手順もそのまま通る。

フローの比較

選択肢は 3 つ並ぶが、実際に迷うのは上の 2 つだけである。判断軸は「クライアントがシークレットを秘密に保てるか」の 1 点に集約される。

フローPKCEクライアントシークレット用途
認可コード + PKCE毎回作る検証値で、認可コードの持ち主を確かめる不要SPA、モバイル (推奨)
認可コード + シークレット使わず、シークレットの提示だけで確かめる必要サーバーサイドアプリ
Implicit (非推奨)使えない。トークンが URL に載るため、後から足しても守れない不要2026 年時点で新規の実装には使わない

Implicit フローはアクセストークンが URL フラグメントに載るため、ブラウザの履歴や中継先から漏れる筋道を塞ぎきれない。RFC 9700 はこの応答型を使わないよう求め、認可コードフローへの移行を指示している。パブリッククライアントでは PKCE の併用が必須、機密クライアントでも推奨であり、新規の実装で PKCE を外す理由は残っていない。

実装でつまずくのは、たいてい verifier の保管場所と認可サーバー側の強制の 2 点である。前者はリダイレクトをまたいで残り、かつ他のスクリプトから読まれにくい置き場を選ぶ。後者は認可サーバーメタデータの code_challenge_methods_supported で対応方式を確かめ、PKCE を必須にできる設定があれば有効にする。この 2 つを押さえれば、残りは定型どおりに組める。

この記事は役に立ちましたか?

関連用語

関連する記事