この記事の結論攻撃者が欲しいのは、リダイレクトで返る一度きりの authorization_code です。PKCEは、認可リクエストの前に正規アプリだけが知る code_verifier を用意し、そのハッシュである code_challenge を認可コードに結び付けます。コードだけを盗んだ攻撃者は、トークン交換時の照合を通過できません。

攻撃者は何をしたいのか

攻撃者のゴールは、ユーザーのパスワードを直接盗むこととは限りません。ログイン後に認可サーバーから正規アプリへ返される authorization_code を横取りし、それを自分でトークンエンドポイントへ送って access_token を得ることです。

典型的には、カスタムURLスキームを使うモバイルアプリのリダイレクトで、悪意のあるアプリが同じスキームのハンドラとして登録されます。認可サーバーとのTLS通信を破る必要はありません。OS内のアプリ間リダイレクトでコードを受け取れれば、攻撃が成立する余地があります。

攻撃者が欲しいものそれを使ってしたいこと
authorization_codeトークンエンドポイントでコードを交換し、ユーザーの権限でAPIを呼び出す
client_id など公開情報正規アプリになりすまして交換リクエストを作る。ネイティブアプリに埋め込んだclient_secretは秘密として扱えない

PKCEのフローを図で見る

正規アプリは、認可リクエストごとにランダムな code_verifier を生成します。サーバーへ送るのはその値そのものではなく、S256で変換した code_challenge です。

sequenceDiagram
  autonumber
  participant App as 正規アプリ
  participant Browser as ブラウザ / OS
  participant Auth as 認可サーバー
  participant Evil as 攻撃者(悪性アプリ)

  App->>App: code_verifierを生成
  App->>App: code_challenge = BASE64URL(SHA-256(verifier))
  App->>Browser: 認可リクエスト(state + code_challenge + S256)
  Browser->>Auth: 認可リクエストを転送
  Auth-->>Browser: ユーザー認証・同意
  Auth-->>Browser: authorization_code
  Browser-->>App: redirect_uriへcodeを返す
  Browser-->>Evil: codeを横取りされる
  Evil->>Auth: code + 偽のcode_verifier
  Auth-->>Evil: 拒否(invalid_grant)
  App->>Auth: code + 本物のcode_verifier
  Auth->>Auth: S256(verifier)とchallengeを照合
  Auth-->>App: access_token / refresh_token

PKCEはどうやって防ぐのか

認可サーバーは、認可コードを発行するときに code_challenge と変換方式 S256 をコードへ関連付けます。トークンエンドポイントでクライアントが code_verifier を送ると、サーバーは同じ計算をやり直します。

BASE64URL(SHA256(ASCII(code_verifier))) == code_challenge

攻撃者が横取りできたのは authorization_code だけです。本物の code_verifier は正規アプリ内にしかなく、認可リクエストに送られるのは一方向変換後の値です。そのため、攻撃者が偽の値を送ると照合に失敗し、サーバーは invalid_grant としてトークンを発行しません。

PKCEが守る境界PKCEが守るのは「認可コードを盗んだだけの攻撃者が、コードをトークンへ交換すること」です。認可サーバーのなりすまし、端末そのものの乗っ取り、XSSで正規アプリの実行環境を完全に支配された場合まで解決する仕組みではありません。

実装で押さえる3つの点

第一に、クライアントがS256を使えるなら code_challenge_method=S256 を指定します。plain は検証前の値がそのままchallengeになるため、互換性が必要な場合を除いて選びません。

第二に、code_verifier は認可リクエストごとに暗号学的に安全な乱数から生成し、認可サーバーへ送らないままトークン交換まで保持します。

第三に、PKCEと state は役割が違います。PKCEは認可コードとトークン交換を結び付け、state はブラウザ側の認可リクエストとレスポンスの対応を確認します。OAuthの実装では、リダイレクトURIの厳密な登録やTLSと合わせて使います。

まとめ

PKCEは、認可コードを「拾った人」ではなく、最初に認可リクエストを開始した正規クライアントだけがトークン交換を完了できるようにする仕組みです。攻撃者の狙いを code から token への交換と捉えると、PKCEが必要な理由と、code_verifier が担う役割が見えやすくなります。

仕様の詳細は RFC 7636(PKCE)、OAuth 2.0の基本フローは RFC 6749、現在のセキュリティ推奨事項は RFC 9700 を参照してください。