認可コードフローとは?認可リクエストからトークン交換までの実装手順と検証設計を解説
ログイン連携の実装で最初に選ぶことになるのが認可コードフローです。RFC 6749の4.1節が定めるこの手順は、ブラウザのリダイレクトで一度「認可コード」を受け取り、それをサーバ間の通信でアクセストークンへ交換する2往復構成を採ります。本記事では、認可リクエストに載せるパラメータの決め方、redirect_uriを完全一致で登録する運用、トークン交換で認可サーバ側が照合している項目、そして実装で手が止まる箇所とエラー応答の読み分けを、実装の順序どおりに整理します。
まとめ:認可コードフローの採用条件と実装で外せない検証点
結論を先に置きます。認可コードフローは「ブラウザを経由してユーザーの同意を取り、アクセストークンだけはブラウザに触れさせない」ための手順です。画面上で同意を得る必要がある連携なら、クライアントがサーバサイドWebでもSPAでもモバイルアプリでも、選ぶのはこのフロー1つに絞られます。同意が介在しないサーバ間連携はclient_credentials、画面を持たない機器はデバイス認可グラントへ寄せる切り分けです。
実装で外せない検証点は4つです。redirect_uriを完全一致で登録して照合すること、stateを発行して戻り値と突き合わせること、認可コードを1回しか使えないよう消費済みフラグを持つこと、そして交換をサーバ間またはPKCEを伴うリクエストで行うこと。1つでも抜けると前提が崩れます。
手が止まりやすいのは仕様の理解ではなく運用の細部です。環境ごとにredirect_uriが増える問題、戻る操作で同じ認可コードが2回届く問題、2つのエンドポイントのエラーを取り違えて再ログインループを作る問題の3つを、先に設計しておきます。
認可コードフローの定義とRFC 6749が認可コードを挟む理由
まず、何を受け渡す手順で、どこを守っているのかを押さえます。
認可コードを介する2往復構成が防いでいるアクセストークン漏えい経路
認可コードフローは、RFC 6749(2012年10月発行)の4.1節が定める認可グラントの1つです。クライアントは認可エンドポイントへユーザーをリダイレクトさせ、同意後に短命の文字列である認可コードを受け取ります。次にそのコードをトークンエンドポイントへ送り、アクセストークンと交換する。この2段構えが核心です。
なぜ1回で済まさず、コードを挟むのでしょうか。理由はアクセストークンの通り道にあります。認可エンドポイントからの戻りはブラウザのリダイレクトで、パラメータはURLに載ります。ここへトークンを直接置くと、履歴、Refererヘッダ、プロキシやサーバのログという、本来残したくない場所に残ってしまう。コードだけを載せてトークンはサーバ間で受け取れば、露出するのは1回しか使えない短命の文字列に限られます。OAuth 2.0が定めるグラント種別の全体像はOAuth 2.0の認可フローと認証・認可の違いで整理しています。
登場人物4者と認可エンドポイント・トークンエンドポイントの役割
手順を追う前に、誰が何を持っているかを固定します。登場するのは4者です。
| 登場人物 | 役割 | この手順で受け取るもの |
|---|---|---|
| リソースオーナー | データの持ち主 | 同意画面と認可の判断 |
| クライアント | 連携したいアプリ | 認可コードとトークン |
| 認可サーバ | 同意取得とトークン発行 | 認可要求と交換要求 |
| リソースサーバ | API本体 | アクセストークンのみ |
認可サーバが公開する窓口は2つに分かれます。認可エンドポイントはブラウザがアクセスする画面つきの窓口で、ユーザー認証と同意画面を担う。トークンエンドポイントはクライアントのプログラムがHTTPで叩くAPIで、画面は持ちません。混同すると、リクエストの組み方もエラーの形式も噛み合わなくなります。前者はリダイレクトで、後者はJSONでエラーを返すためです。
implicitとROPCが選択肢から外れOAuth 2.1が現在採る方針
かつてはresponse_type=tokenでトークンを直接受け取るインプリシットフローや、ユーザーのIDとパスワードをクライアントが預かるリソースオーナーパスワードクレデンシャルズ(ROPC)も選択肢でした。今は事情が違います。RFC 9700「Best Current Practice for OAuth 2.0 Security」(BCP 240・2025年発行)は、これらを使わないよう求める側に立ちました。
仕様の統合作業も同じ方向です。OAuth 2.1の草案は本稿執筆時点で draft-ietf-oauth-v2-1 の15版(2026年3月更新)まで進み、インプリシットとROPCを本文から削除してPKCEを全クライアントへ必須とする構成です。同意を取るフローは認可コードフローへ一本化された、と捉えて構いません。
認可リクエストで送るパラメータの決め方とredirect_uriの登録
ここから実装に入ります。最初の1本目のリクエストで、後続すべての検証条件が決まります。
認可エンドポイントへ渡すパラメータ6種と値を決める実装手順の整理
クライアントは認可エンドポイントのURLへクエリを付け、そこへブラウザを飛ばします。載せる値は次の6つです。
response_type=code— 認可コードを要求する宣言。tokenならインプリシットになるclient_id— 事前登録したクライアントの識別子(秘密情報ではない)redirect_uri— 同意後に戻す先。登録済みの値と完全一致させるscope— 要求する権限の範囲。後から拡大できないため過不足なく指定するstate— クライアントが生成する推測不能な値。戻り値と突き合わせるcode_challengeとcode_challenge_method— PKCEの検証値。方式はS256
値の決め方には順序があります。client_idとredirect_uriは認可サーバへの登録が先で、コード側は登録済みの値を読むだけにしてください。scopeは後から絞るのが難しいため、必要な操作を洗い出してから決めます。stateとcode_challengeはリクエストごとに生成し、セッションと紐づけて保存する。保存を忘れると戻り時点で照合できません。code_verifierの生成方法と認可サーバ側の検証義務はPKCEのcode_verifier実装と検証設計に切り出しました。
redirect_uriを完全一致で登録する運用と可変値の受け渡し
事故が起きやすいのはこのパラメータです。RFC 9700は、認可サーバがredirect_uriを事前登録された値との完全な文字列一致で照合すべきだとしています。前方一致やワイルドカードを許すと、攻撃者が同じドメイン配下の任意のパスへ認可コードを飛ばせる余地が生まれるためです。パスの一部をパターンで許可する設定は、便利に見えて穴になります。
実装者が困るのは、戻り先を動的に変えたい場合です。「ログイン後に元いた画面へ戻す」という要件は、redirect_uriにクエリを足して解こうとすると完全一致が崩れます。解き方は、戻り先URLをサーバ側のセッションかstateに対応づけて保存し、redirect_uri自体は固定の1本にしておくこと。保存済みの戻り先を引き当てて内部的に遷移させれば、画面が増えても登録するURIは増えません。
stateとnonceの役割の違いと保存先を決める判断の基準
似た位置に置かれる2つの値ですが、守る対象が違います。混同すると片方が実質的に無効化されます。
| パラメータ | 目的 | 照合するタイミング |
|---|---|---|
| state | CSRFと戻り先の紐づけ | コールバック受信時 |
| nonce | IDトークンの再利用防止 | IDトークン検証時 |
| code_verifier | 認可コード横取りの防止 | トークン交換時 |
stateはOAuth 2.0の範囲、nonceはOpenID Connectの範囲という違いもあります。IDトークンでユーザーを識別する構成なら両方を送り、アクセストークンだけが目的ならstateとPKCEで足りる。OpenID Connect側の要件はOIDCの仕組みとOAuthとの違いを先に押さえておくと判断しやすくなります。
保存先の判断基準は「コールバックのリクエストからサーバ側で読めるか」の一点です。サーバサイドWebならサーバのセッションが素直な置き場所になります。SPAで独自にコールバックを処理する構成では、リロードで消えると照合できないためsessionStorageへ置く。stateは「送った値と戻った値が同じか」だけを見る用途に限ってください。
認可コードの受け取りとトークン交換で認可サーバが検証する項目
2往復目に入ります。ここで認可サーバは、1往復目の内容とリクエストの整合を照合しています。
トークンエンドポイントへのPOST内容と応答で確認する4項目
同意後、認可サーバはredirect_uriへcodeとstateを付けてブラウザを戻します。クライアントはまずstateを照合し、一致した場合にだけ交換へ進みます。交換リクエストの中身は次のとおりです。
- トークンエンドポイントへPOSTし、本文はフォーム形式で送る(JSONではない)
grant_type=authorization_codeとcode、code_verifierを載せるredirect_uriを再度送る(認可リクエストの値と一致するか照合される)- 機密クライアントはあわせてクライアント認証を行う
応答で確認すべきは4項目です。access_tokenの値、token_type、有効期限を示すexpires_in、そしてrefresh_tokenが同梱されているか。OpenID Connectを使う構成ではid_tokenの署名検証も加わります。更新以降の設計、寿命の決め方とローテーションはリフレッシュトークンの更新フローと寿命設計で扱いました。
認可コードの一回性と寿命10分以内という推奨値を守る実装設計
認可コードには2つの制約があります。RFC 6749の4.1.2節は、認可コードを1回より多く使ってはならないとし、有効期間は最大10分を推奨値として示しています。実装ではこの2つを別の仕組みでそれぞれ担保してください。
寿命のほうは発行時刻を保存して交換時に差分を見るだけで済みます。一回性は消費済みの状態を記録しないと成立しません。認可サーバを自作する場合、コードのレコードに消費済みフラグを持たせ、交換処理をトランザクション内で「未消費のものを消費済みへ更新して1件更新できたか」の判定に落とし込む。単に「確認してから更新する」2ステップにすると、同時到達した2本が両方通る余地が残ります。
再利用を検知したときの扱いも決めておいてください。RFC 6749は、そのコードから発行済みのトークンも取り消すべきだとしています。1回目が正規のクライアント、2回目が攻撃者という順序も、その逆もあり得るためです。
機密クライアントと公開クライアントで変わるクライアント認証の選定
トークンエンドポイントで自分を証明できるかどうかで、クライアントは2種類に分かれます。サーバサイドで秘密情報を保持できるものが機密クライアント、ブラウザやアプリのように配布物へ秘密を埋め込むしかないものが公開クライアントです。SPAとモバイルアプリは後者に該当します。配布物から取り出せる値は、秘密として機能しないためです。
機密クライアントの認証方式は、認可サーバが対応する範囲から選びます。client_secret_basicはAuthorizationヘッダに載せる方式、client_secret_postは本文に載せる方式で、どちらも共有秘密を前提とする。共有秘密を避けるなら、署名つきJWTを使うprivate_key_jwtや相互TLSが選択肢です。鍵のライフサイクル管理を回せる体制があるかが分かれ目になります。
公開クライアントは秘密を持てないため、PKCEがその代わりを担います。ここを省くと、認可コードを奪った第三者がそのまま交換できてしまいます。
認可コードフローの実装で手が止まる3箇所とエラー応答の切り分け
仕様どおりに書いても、ここで詰まります。3箇所とも仕様書には書かれていない運用側の問題です。
認可エンドポイントとトークンエンドポイントのエラー応答の違い
2つの窓口はエラーの返し方が違います。認可エンドポイントは、redirect_uriが検証できた場合に限り、そのURIへerrorを付けてブラウザをリダイレクトします。client_idやredirect_uriそのものが不正なときは、リダイレクトせず認可サーバ側でエラー画面を出す。攻撃者が指定した先へ情報を渡さないための挙動です。
トークンエンドポイントはHTTPステータスとJSONで返します。切り分けの要点は3つです。invalid_grantは認可コードの期限切れ、消費済み、またはredirect_uriの不一致で、リトライしても結果は変わらない。invalid_clientはクライアント認証の失敗で、秘密情報の設定ミスを疑います。invalid_requestはパラメータの不足や重複を示します。
invalid_grantをリトライ対象にすると、再認可と失敗が交互に続く無限ループを作ります。実装は、認可エンドポイント由来のaccess_deniedなら「同意されなかった」旨の画面へ、invalid_grantならセッションを破棄してログイン画面へ1回だけ戻す、それ以外は障害としてログに残す、という3分岐で足ります。
ブラウザの戻る操作による認可コードの二重送信を止める実装設計
実運用で最初に踏むのがこれです。コールバックURLで認可コードを受け取り、そのまま交換を行ってから画面を描画する実装だと、ユーザーが戻るボタンを押すかリロードした瞬間に、同じコードで2回目の交換が走ります。認可サーバは正しくinvalid_grantを返しますが、画面にはエラーが出ます。すでにログインは成功しているのに、です。
対処は2段構えにします。第一に、処理を終えたら結果画面へリダイレクトし、認可コードを含むURLを履歴に残さない。第二に、入口でstateをセッションから取り出したら即座に削除し、2回目以降は処理済みの分岐へ落とす。その分岐でログイン済みなら、エラーを出さずアプリ画面へ送ってください。ログイン後のセッション自体の持ち方はセッションIDの発行から失効までの実装と揃えておくと、失効経路が二重にならずに済みます。
環境やテナントごとにredirect_uriが増える構成の設計
完全一致で登録する原則を守ると、環境の数だけURIが増えます。ローカル開発、検証、本番、さらにプレビュー環境が動的に立つ構成なら、登録作業が追いつきません。ここを「ワイルドカードを許可して解決」としてはいけません。前述の穴を自分で開ける行為です。
採るべき手は2つあります。1つは環境ごとに別のクライアント登録を持つこと。client_idを分ければ、本番の登録に開発用URIが混ざりません。もう1つは、固定のコールバック用中継点を1つ用意し、そこからstateに紐づけた戻り先へ内部遷移させる方式です。テナントごとにサブドメインが変わる構成も後者で扱えます。テナント識別子はredirect_uriに載せず、state側で運んでください。
認可コードフローを採用する条件と別のグラントへ切り替える判断
ここからは仕様が答えを持たない領域です。条件を置いて言い切ります。
採用条件3つとclient_credentialsやデバイス系へ寄せる場面
認可コードフローを選ぶ条件は3つです。第一に、ユーザー本人が画面上で同意する必要がある連携であること。第二に、ブラウザのリダイレクトを扱える環境であること。第三に、データの持ち主がクライアントの運営者ではなくユーザー自身であること。3つ揃うなら、クライアントの種別を問わずこのフローです。
見送る場面も明確です。自社のバッチ処理が自社のAPIを叩く連携は、同意する人間がいないためclient_credentialsグラントを使います。テレビやIoT機器のように文字入力とブラウザ表示が難しい端末は、デバイス認可グラントへ寄せる。判断の軸は「同意する人間がいるか」「ブラウザが使えるか」の2問だけです。
SPAでBFFへ寄せるかブラウザ内で完結させるかの判断ライン
迷いが残るのはSPAです。ブラウザ内でPKCE付きの認可コードフローを完結させる構成と、サーバ側にBFF(Backend For Frontend)を置いてトークンをそこへ閉じ込める構成の2択になります。どちらも仕様上は成立するため、判断材料を先に決めておきます。
線引きはこうです。扱うデータの機微度が低く、トークンをメモリのみに保持してタブを閉じたら失う運用が許容できるなら、ブラウザ内で完結させて構いません。逆に、決済や個人情報を扱う、長時間のセッション維持が要件、あるいはXSSが1つ通ったときの被害を許容できないなら、BFFへ寄せます。BFFではブラウザとの間をHttpOnlyクッキーにし、トークンはサーバ側だけが持つ形にしてください。判断を先送りすると、移行時にトークンの保存箇所と失効経路を全面的に書き換えることになります。
レビューで差し戻すstate未検証とredirect_uri前方一致
実装レビューで見つけたら差し戻す構成が2つあります。1つはstateを送っているのに戻り値を照合していない実装です。生成して付けるところまでは書かれていて、コールバック側で読んでいない。攻撃者が用意した認可コードを被害者のセッションへ紐づけられる状態で、送っていないのと変わりません。もう1つはredirect_uri照合を前方一致にした設定で、同一ドメイン配下に外部から書ける画面が1つでもあれば、そこへ認可コードを流し込めます。
どちらも難所ではなく、単に抜けやすい箇所です。コールバック処理の先頭でstate照合とcode_verifierの取り出しを行い、照合失敗なら即座に打ち切る。認可サーバ側は完全一致に設定してください。会員基盤を自前で持ち、認証・認可の設計から実装まで含めて相談したい場合は会員管理システム開発で、要件整理から対応しています。
よくある質問
認可コードフローの実装で相談の多い5点をまとめます。
認可コードフローとインプリシットフローはどちらを使うべきですか?
新規実装では認可コードフローを選んでください。インプリシットフローはアクセストークンをブラウザのリダイレクトで直接受け取る方式で、URLの断片に載るトークンが履歴やログに残ります。RFC 9700は使用を避けるよう求め、OAuth 2.1の草案では本文から削除されました。SPAであっても、PKCEを併用した認可コードフローが現在の標準的な組み方です。
認可コードの有効期限はどれくらいに設定すればよいですか?
RFC 6749は最大10分を推奨値として示しており、実際には1分から5分程度に絞っている実装も珍しくありません。認可コードは受け取ってから即座に交換へ回すため、短くしても運用上の不都合はほとんど出ません。期限より効くのは一回性の担保です。消費済みフラグをトランザクション内で更新して、同時到達した2本が両方通らないようにしてください。
redirect_uriに動的なパラメータを付けても問題ありませんか?
避けてください。認可サーバは登録済みの値と完全一致で照合するため、クエリが1つ増えただけで交換が通らなくなります。一部の認可サーバが緩く扱ってくれたとしても、そこへ依存した実装は移植性を失う。戻り先を動的に変えたい場合は、URLをサーバ側のセッションに保存してstateと対応づけ、redirect_uri自体は固定の1本にしてください。
invalid_grantが返るときは何を確認すればよいですか?
確認は4点の順で行います。第一に認可コードの期限切れ。第二に同じコードを2回送っていないか(戻る操作やリロードが典型です)。第三に交換リクエストのredirect_uriが認可リクエストのものと完全に一致しているか。第四にPKCEのcode_verifierが認可リクエスト時の値と対応しているか。リトライしても回復しないエラーなので、ループを組まず一度だけログイン画面へ戻します。
SPAで認可コードフローを使う場合トークンはどこに保存しますか?
ブラウザ内で完結させる構成なら、メモリ上に保持してタブを閉じたら失う方式が最小限の防御です。localStorageはJavaScriptから読めるため、XSSが1つ通れば持ち出されます。長時間のセッション維持や機微なデータを扱うなら、BFFを置いてトークンをサーバ側へ閉じ込めてください。分かれ目はデータの機微度と許容できる被害範囲の2点です。
関連記事
- JWTとは?構造・署名検証の仕組みとセッション・OAuth/OIDCとの違いを実装視点で解説:トークンの中身を検証する実装に進むときに。
- 認可バイパスとは?IDOR・権限昇格の仕組みとサーバ側で止める実装対策を解説:トークン取得後のAPI側の権限判定を設計するときに。
- IDaaSとは?SSO・IdPとの違いとSAML・OIDC・SCIM連携を実装視点で解説:認可サーバを外部サービスへ寄せる判断に。
- CIAMとは?顧客ID基盤の設計と会員DB移行の同意管理を実装視点で解説:顧客向けログイン基盤ごと設計し直す場合の入口。
- APIセキュリティとは?認証・認可からレート制限まで実装者目線で解説:トークン以外の防御層とあわせて組むときに。