PKCEとは?認可コード横取り攻撃を防ぐcode_verifierの実装と検証設計を解説
モバイルアプリのログイン実装をレビューすると、認可コードを受け取る部分だけが素通りになっていることがあります。PKCE(Proof Key for Code Exchange)は、その認可コードを横取りされてもアクセストークンを渡さないためのOAuth 2.0の拡張仕様です。RFC 7636として2015年9月に発行され、2025年1月に出たRFC 9700では公開クライアントへの適用が必須と位置づけられました。本記事では、code_verifierとcode_challengeの生成規則、認可サーバ側に生じる検証義務、SPA・ネイティブアプリ・機密クライアントという種別ごとの保存設計、そしてPKCEだけでは守り切れずBFF構成へ倒すべき条件までを、実装の順序で整理します。
まとめ:PKCEの適用範囲とクライアント・認可サーバ双方の実装結論
結論を先に置きます。PKCEは公開クライアント向けの追加オプションではなく、認可コードフローの既定要件へ移りました。RFC 9700(BCP 240・2025年1月発行)は、公開クライアントに対してPKCEの使用を、認可サーバに対してPKCEのサポートを、いずれもMUSTで規定しています。機密クライアントにもSHOULDで推奨されており、入れるかどうかを検討する段階は終わっています。
クライアント側の作業量そのものは小さい。32バイトの乱数をbase64urlで符号化してcode_verifierを作り、そのSHA-256ハッシュをcode_challengeとして認可リクエストへ付け、トークンリクエストで元の値を送り返すだけでした。手が止まるのは生成処理ではなく、verifierをどこへ置くか、そして認可サーバ側で検証を必須にできるかという2点にあります。
見送る条件はほぼありません。plainを選ぶのはSHA-256を実装できない実行環境に限られます。判断が要るのはPKCEの採否ではなくその先で、ブラウザ上にアクセストークンを置き続けるか、サーバ側へ閉じ込めるBFF構成へ移すかという分岐です。後半の独自章で3つの条件に分解します。
PKCEの定義とRFC 7636が塞ぐ認可コード横取り攻撃の成立条件
PKCEは認可コードフローに証明を1つ足す仕組みです。何を防ぎ、何を防がないかを切り分けます。
認可コードフローに残る窃取経路とPKCEが遮断する範囲の境界線
認可コードフローでは、ユーザの承認後に認可サーバがリダイレクトで認可コードをクライアントへ返します。この折り返しが弱点でした。ネイティブアプリはクライアントシークレットを端末内に安全に保持できないため、認可コードさえ手に入れば誰でもトークンエンドポイントを呼べる。同じカスタムURIスキームを登録した悪意あるアプリがリダイレクトを先取りすれば、正規アプリの代わりにアクセストークンを受け取れます。
PKCEが遮断するのはこの経路だけです。認可リクエストを出した主体しか知らない秘密をトークン交換時に要求するため、コードを拾っただけの攻撃者は交換に失敗する。裏を返せば、発行済みアクセストークンの漏洩、リダイレクトURIの照合不備、XSSによるトークン読み出しは守備範囲の外にあり、別の対策が要ります。フロー全体の前提はOAuth 2.0の認可フローと認証・認可の違いで整理しました。
code_verifierの生成規則と43文字から128文字という長さ規定の根拠
RFC 7636のcode_verifierは、unreserved characters([A-Z] [a-z] [0-9] とハイフン・ピリオド・アンダースコア・チルダ)だけで構成する43文字以上128文字以下のランダム文字列と定義されています。実務で効くのは上限より下限のほうでした。32バイト、つまり256ビットの乱数をbase64urlで符号化するとパディングを除いて43文字になり、下限はこの長さをそのまま採っています。
43文字は「短くても43文字あればよい」ではなく、「256ビットの乱数を符号化した結果」だと理解してください。文字数の条件だけを満たそうとしてユーザIDやタイムスタンプを連結すると、仕様上は通るのに推測耐性が落ちる。生成は乱数バイト列から始めます。
S256とplainの差分とplainの使用を許容できる実行環境の条件
code_challenge_methodが取る値はplainとS256の2つです。plainはチャレンジにverifierをそのまま入れる方式で、認可リクエストのURLに秘密が生のまま載る。ブラウザ履歴、プロキシのアクセスログ、OSのURL処理、リファラのいずれか1つから漏れた時点で、PKCEを入れた意味が消えます。
S256はSHA-256ハッシュを渡すため、認可リクエストが漏れてもverifierまでは復元できません。RFC 7636はS256の実装を必須と定め、plainはSHA-256を実装できない環境向けの互換手段として残しました。RFC 9700も、verifierを露出しないチャレンジ方式を使うようクライアントへ求めています。判断としては、plainを受け付ける設定を認可サーバ側で閉じてください。
認可リクエストからトークン交換までの検証点と失敗時のエラー応答
PKCEの往復は4手です。どの値をどこで突き合わせ、外れたときに何が返るのかを押さえます。
認可リクエストに付与する2つのパラメータと認可コードへの紐づけ設計
クライアントが認可エンドポイントへ渡す追加パラメータはcode_challengeとcode_challenge_methodの2つだけです。認可サーバはこの2値を受け取り、発行する認可コードと1対1で結びつけて保存します。手順にすると次の流れになります。
- クライアントが乱数からcode_verifierを生成し、自身の一時領域へ保存する
- SHA-256でハッシュ化しbase64urlで符号化してcode_challengeを作る
- 認可リクエストへcode_challengeとmethodを付けて認可エンドポイントへ送る
- 認可サーバが発行する認可コードにchallengeとmethodを紐づけて保持する
保持先はセッションではなく認可コードそのものに紐づけます。セッションへ置くと、認可サーバを複数台に並べた構成やアプリとブラウザでセッションが分かれる構成で突合が崩れます。
トークン交換でのcode_verifier送信とinvalid_grantが返る条件
トークンエンドポイントへは認可コードと合わせてcode_verifierを送ります。認可サーバは保存しておいたmethodに従ってverifierを変換し、保存済みのchallengeと一致するかを比較する。一致しなければRFC 7636の規定によりinvalid_grantを返し、アクセストークンは発行されません。
エラーの出し分けも決まっています。認可エンドポイントの時点でcode_challengeが欠けるなど要求が不正ならinvalid_request、トークン交換の照合失敗ならinvalid_grant。この2つを取り違えるとクライアント側の原因究明が長引きます。ログには照合失敗の事実だけを残し、verifierやchallengeの値は出力しないでください。
stateとnonceが担う防御の違いとPKCEで代替できない範囲の切り分け
PKCEを入れるとstateが不要になるという整理を見かけますが、正確ではありません。3つの仕組みは検証する場所も防ぐ攻撃も別々です。
| 仕組み | 防ぐ攻撃 | 検証する場所 |
|---|---|---|
| PKCE | 認可コードの横取り | トークンエンドポイント |
| state | ログインCSRF | クライアントのコールバック |
| nonce | IDトークンの再利用 | IDトークンの検証時 |
PKCEは認可コードとverifierの結びつきを認可サーバ側で確かめる仕組みで、攻撃者のコードを被害者のブラウザへ流し込むログインCSRFは防ぎません。OAuth 2.1のドラフト(draft-ietf-oauth-v2-1-15・2026年3月2日更新の版)はCSRF対策としてPKCEを認める一方、リダイレクト後に元の画面へ戻すアプリ独自の状態復元はstateの仕事として残ります。IDトークンのリプレイを防ぐnonceはOpenID Connect側の仕様で、詳細はOpenID Connectの仕組みとOAuthとの違いにまとめました。
認可サーバ側の実装義務とPKCEダウングレード攻撃を遮断する条件
PKCEはクライアントだけ直しても効きません。検証を担う認可サーバ側に、3つの実装義務が生まれます。
サーバメタデータでのチャレンジ方式公開とS256強制に踏み切る判断
認可サーバは、対応するチャレンジ方式をAuthorization Server Metadata(RFC 8414)のcode_challenge_methods_supportedで公開します。RFC 9700はこの公開をSHOULDとしており、クライアントライブラリの多くはこの値を見てPKCEを有効にするかを決める。ここを空のまま返すと、ライブラリ側がPKCE非対応と判断して黙って外すことがあります。
公開する値はS256だけに絞ります。plainを併記すると、クライアント実装の初期値がplainのままだった場合に弱い方へ倒れる。互換のためにplainを残すなら、対象クライアントIDの許可リストで限定し、外す期限まで同時に決めてください。
PKCEダウングレード攻撃の成立条件とcode_verifier必須化の実装手順
PKCEダウングレード攻撃は、認可リクエストからchallengeを取り除く、あるいはトークンリクエストからverifierを省くことで、サーバ側の照合処理そのものを迂回する手口です。認可サーバが「verifierが送られてきたときだけ検証する」実装になっていると成立してしまう。遮断は次の順序で入れます。
- 認可コードにchallengeの有無をフラグとして記録する
- フラグが立つコードにverifier無しのトークンリクエストが来たら拒否する
- フラグが立たないコードにverifierが付いてきた場合も拒否する
- チャレンジ方式は保存済みの値を使い、リクエスト側の申告を信用しない
4番目を落としている実装をときどき見かけます。トークンリクエストのmethod申告どおりに変換すると、攻撃者はS256で登録されたコードにplainを申告し、盗んだchallengeをそのままverifierとして送れる。変換方式は必ず認可コード側に保存した値を使ってください。サーバ側の検証を素通しにする類型は認可バイパスの仕組みとサーバ側で止める実装対策でも扱っています。
既存クライアントを壊さずPKCE必須へ寄せる段階移行と切り戻し条件
稼働中の認可サーバでいきなり必須化すると、更新の遅いモバイルアプリが一斉にログインできなくなります。移行は計測から始めるのが安全でした。クライアントIDごとにPKCE付きリクエストの比率を記録して未対応のものを特定し、通知を出したうえで、対応済みのものから単位ごとに必須フラグを立てます。
切り戻しの条件も先に決めます。必須化の適用後に当該クライアントの認証成功率が平常時から目に見えて落ちたら、原因調査より先にそのクライアントだけフラグを戻す。全体を一括で戻す運用にすると、対応済みのものまで保護が外れてしまいます。
クライアント種別ごとのcode_verifier保存先とリダイレクト方式の選定
verifierの置き場所は種別ごとに答えが変わります。SPA、ネイティブアプリ、機密クライアントの順に見ていきます。
SPAでのcode_verifier保存先とsessionStorageを選ぶ条件
SPAでは認可サーバへのリダイレクトでページが破棄されるため、verifierをメモリだけに持てません。現実的な保存先はsessionStorageで、タブを閉じれば消え他タブと共有されない性質が、認可の1往復という寿命に合っています。localStorageは永続するうえ全タブで共有されるため、この用途には過剰です。
ただしJavaScriptから読める領域である以上、XSSが起きればverifierも読まれます。ここは割り切りが必要でした。読み出されて困る度合いが高いなら、保存先を変えるのではなく後半で扱うBFF構成へ移す判断になる。コールバック処理の直後にverifierを削除する後始末は、どの構成でも入れてください。
ネイティブアプリの3方式とRFC 8252が示すリダイレクト先の優先順位
ネイティブアプリのリダイレクト方式はRFC 8252(BCP 212・2017年10月発行)が3種類を挙げ、認可サーバにはいずれもサポートを求めています。選定の軸は「そのリダイレクト先を他アプリに横取りされないか」の1点です。
| リダイレクト方式 | 登録の一意性 | 採用する場面 |
|---|---|---|
| プライベートURIスキーム | OSが保証しない | 他方式が使えない場合 |
| クレームドHTTPSスキーム | ドメイン所有で保証 | モバイルアプリの既定 |
| ループバック | ポート単位で占有 | デスクトップとCLI |
RFC 8252は、ユニバーサルリンクやアプリリンクにあたるクレームドHTTPSスキームを可能なら使うべきとしています。プライベートURIスキームは同名を別アプリが登録でき、横取りが起きる経路そのもの。逆順ドメイン形式の命名でも、OSレベルの保証はありません。この方式を選ぶ場合こそPKCEが最後の砦になります。
機密クライアントへPKCEを足す意味とRFC 9700が広げた適用範囲
クライアントシークレットを安全に保持できるサーバサイドWebアプリでは、PKCEは長らく任意とされてきました。RFC 9700はこの前提を広げ、機密クライアントを含むすべての種別へ適用するよう求めています。理由は認可コードインジェクションで、攻撃者が自分の認可コードを被害者のセッションへ差し込む攻撃はシークレットの有無と無関係に成立するためです。verifierはサーバのセッションストアへ置けるため、SPAで残る読み出しリスクも生じない。保存先や失効の設計はセッションIDの発行から失効までの実装と保存先選定を参照してください。
PKCE単体で守り切れる範囲とBFF構成へ切り替える判断ライン
ここからは判断です。PKCEを入れても残るリスクと、構成を変えるべき条件を示します。
ブラウザにトークンを置く構成で残るXSS経路と被害範囲の差分
PKCEを入れたSPAでも、取得後のアクセストークンとリフレッシュトークンはブラウザ内にあります。XSSが1つ通れば、攻撃者はverifierを盗む必要すらなくトークンを直接読んで持ち出せる。PKCEは認可コードの受け渡し区間を守る仕組みであって、受け取った後の保管を守るものではありません。
被害範囲の差も見ておきます。トークンがブラウザにある構成では、盗まれたリフレッシュトークンで攻撃者の環境から任意のタイミングでアクセストークンを再発行できる。サーバ側に置く構成なら盗めるのはセッションクッキーまでで、被害はそのブラウザが動いている間に限られます。
BFFへ倒す3条件とPKCEと短命トークンで踏みとどまってよい境界
BFF(Backend for Frontend)へ切り替えるべき条件は3つに絞れます。第一に、扱うAPIが決済・個人情報・権限変更のいずれかを含む場合。第二に、リフレッシュトークンの寿命を数時間より長く取らざるを得ない場合。第三に、サードパーティ製スクリプトを画面に読み込んでいる場合です。いずれか1つでも当てはまるなら、トークンをブラウザから外してください。
逆に、社内向けの参照系ダッシュボードで、アクセストークンの寿命が15分程度、リフレッシュトークンをローテーション方式にでき、外部スクリプトを載せていないなら、PKCE付きの認可コードフローのまま踏みとどまってよい。BFFはセッション管理とAPI中継を自前で抱える構成で、守るものが軽い画面へ入れると運用の手間だけが増えます。
実装で照合が通らなくなる箇所と本番投入前に踏む検証手順の設計
照合が通らないという相談は、原因がほぼ決まっています。詰まりやすい箇所を挙げます。
base64urlのパディング除去とASCII前提で生じる文字列の不一致
最も多いのが符号化の取り違えです。RFC 7636のchallengeはbase64urlで、末尾の等号によるパディングを付けません。通常のbase64を使うと、プラス記号とスラッシュがそのまま残りパディングも付いたまま送られ、認可サーバ側の値と1文字も一致しなくなります。
もう1つがハッシュ対象の指定です。仕様はverifierのASCII表現に対してSHA-256を取ると定めています。言語によってはUTF-16やプラットフォーム既定の文字コードでバイト列へ変換するため、明示しないと環境によって結果が変わる。加えて乱数生成器も見てください。Math.randomのような擬似乱数は内部状態が推測でき、暗号用途に使えません。ブラウザならcrypto.getRandomValues、サーバサイドなら各言語の暗号用乱数APIを使います。弱い乱数でも照合は通るため、テストでは問題が出ない点が厄介でした。
認可サーバがPKCE未対応だった場合の代替手段と見送りの判断
接続先の認可サーバがPKCEに対応していないケースは、社内の古い認証基盤や一部の業務パッケージで今も残っています。この場合、クライアント側だけでPKCE相当の保護は作れません。認可サーバが検証しなければ、challengeもverifierも無視されるパラメータで終わるためです。
取れる手は3つ。認可サーバ本体の改修、PKCE対応の認可サーバを前段に挟む構成変更、リダイレクト先を自社サーバに限定して機密クライアントとして扱う構成です。3つとも取れない場合に限り導入を見送り、リダイレクトURIの完全一致照合と認可コードの有効期限短縮で当座をしのぐ判断になる。この状態は暫定なので、期限を切って改修計画へ乗せてください。実装が意図どおり守れているかを外部の目で確かめるなら、脆弱性診断・セキュリティ診断のような検査工程を移行の前後に挟むと設定漏れを拾えます。
よくある質問
PKCE実装で相談の多い論点をまとめます。
PKCEはSPAでも必要ですか?
必要です。SPAはクライアントシークレットを安全に保持できない公開クライアントに当たるため、RFC 9700ではPKCEの使用がMUSTとして規定されています。かつてSPA向けとされたインプリシットフローは、アクセストークンがURLフラグメントに載る構造が問題視され現在は非推奨。認可コードフローにPKCEを組み合わせる形が既定です。ただしXSSへの防御にはならないため、トークンの保管方法は別途検討が要ります。
stateパラメータはPKCEを入れれば省略できますか?
CSRF対策という目的に限れば、OAuth 2.1のドラフトはPKCEで代替できるとしています。一方でstateには、リダイレクトから戻った後に元の画面や操作対象を復元するアプリ固有の用途もあり、そちらは残る。既存実装で使っているなら、PKCEを追加しても外す必要はありません。両方を送っても衝突しません。
code_verifierはどこに保存するのが安全ですか?
クライアントの種別で変わります。サーバサイドWebアプリならサーバのセッションストアが最も安全です。SPAはsessionStorageが現実的な選択で、タブ単位かつ一時的という性質が認可の往復に合う。ネイティブアプリはプロセス内のメモリに保持し、共有領域や平文ファイルには書きません。いずれの場合も、トークン交換の完了時に削除する処理を入れてください。
plainとS256はどちらを選ぶべきですか?
S256を選びます。plainはcode_challengeにverifierをそのまま載せるため、認可リクエストのURLが履歴やログに残った時点で秘密が露出する。RFC 7636もS256の実装を必須とし、plainは実装できない環境向けの互換手段と位置づけています。特殊な事情がない限り、認可サーバ側でplainを受け付けない設定にしてください。
認可サーバがPKCEに未対応の場合はどうすればよいですか?
クライアント側だけでは保護になりません。認可サーバが検証しない以上、送ったパラメータは無視されるためです。選択肢は、認可サーバ本体の改修、PKCE対応の認可サーバを前段に置く構成変更、リダイレクト先を自社サーバに限定して機密クライアントとして扱う構成の3つ。どれも取れない場合の暫定策は、リダイレクトURIの完全一致照合と認可コードの有効期限短縮の組み合わせです。
関連記事
- JWTとは?構造・署名検証の仕組みとセッション・OAuth/OIDCとの違いを実装視点で解説:IDトークンの検証処理を組むときの土台に。
- Ory Hydraとは?OAuth2.0/OIDC認可サーバーの仕組みとDocker構築・認証基盤連携を解説:認可サーバを自前で立てる場合の選択肢。
- SAMLとは?認証フロー・IdP/SPの仕組みとOAuth・OIDCとの使い分けを実装視点で解説:既存のSAML基盤との併存を検討するときの比較材料に。
- IDaaSとは?SSO・IdPとの違いとSAML・OIDC・SCIM連携を実装視点で解説:認可サーバを外部サービスへ寄せる判断に。
- CIAMとは?顧客ID基盤の設計と会員DB移行・同意管理を実装視点で解説:顧客向けログイン基盤ごと設計し直す場合の全体像。