OAuth 2.0 は使うのは簡単でも、全体像を思い描くのは難しいものです。ログインボタン 1 つで 3 者か 4 者の間にリダイレクトが起き、それぞれが似た名前のパラメーターを運びます。そしてバグの多くは、ある当事者がやるべき確認を飛ばすことから生まれます。誰が誰に何をどの順に送るかを見るには、シーケンス図が最もわかりやすい方法です。
この記事では、今日ほぼすべてのクライアントで使うべきフローを図にします。PKCE 付きの認可コードグラントです。各ステップを順に追い、それぞれの当事者が何を確認するかを説明し、現在のガイダンスが廃止を求めている古いフローを挙げます。
4 つの当事者
| 仕様上の役割 | 実際には | 保持するもの |
|---|---|---|
| リソースオーナー | ユーザー | 自分の認証情報 |
| クライアント | Web アプリ、シングルページアプリ、モバイルアプリ | 受け取ったトークン |
| 認可サーバー | ID プロバイダー | ユーザーアカウント、同意、署名鍵 |
| リソースサーバー | API | データと、どのトークンを受け入れるかのルール |
クライアントがユーザーのパスワードを目にすることはありません。それが OAuth の要点です。認可サーバーがユーザーを認証し、スコープと有効期間が限られたトークンをクライアントに渡します。
2026 年に使うべきフロー
最初の OAuth 2.0 フレームワークである RFC 6749 は、いくつものグラントを定義していました。2025 年 1 月に公開された OAuth 2.0 Security Best Current Practice(RFC 9700) は、その選択肢を絞り込んでいます。
- PKCE 付き認可コード: あらゆる種類のクライアントからのユーザーのサインインに使います。パブリッククライアント(モバイルアプリ、シングルページアプリ)は PKCE が必須で、サーバー側にシークレットを持つコンフィデンシャルクライアントにも推奨されます。
- インプリシットグラントは使わない。 ブラウザーのリダイレクトでトークンを返すので、そこから漏れるおそれがあります。クライアントは使うべきではありません。
- リソースオーナーパスワードクレデンシャルグラントは使わない。 ユーザーのパスワードをクライアントに渡すのは本末転倒です。BCP は使用してはならないとしています。
- クライアントクレデンシャル: ユーザーが関わらないサービス間の呼び出しに使います。
OAuth 2.1 のドラフトはこれらのルールを 1 つの文書にまとめているので、新しいライブラリはますますこれを前提にしています。
PKCE を 1 段落で
Proof Key for Code Exchange(RFC 7636) は認可コードを守ります。クライアントはリダイレクトの前にランダムな code_verifier(43〜128 文字)を作り、その SHA-256 ハッシュである code_challenge だけを code_challenge_method=S256 とともに送ります。あとでコードをトークンと交換するとき、クライアントはベリファイアを送ります。サーバーはそれをハッシュして比較します。コードを横取りした攻撃者も、クライアントから一度も出ていないベリファイアがなければ、そのコードを使えません。
フローを順に追う
- ユーザーがサインインをクリックする。 クライアントは
state(ユーザーのセッションに結び付いたランダムな値)と、PKCE のベリファイアとチャレンジを生成します。 - クライアントが
/authorizeにリダイレクトする。response_type=code、client_id、登録済みのredirect_uri、要求するscope、state、コードチャレンジを付けます。 - 認可サーバーがログインと同意の画面を表示する。 クライアントは関与しません。これはサーバーのドメイン上で起きます。
- ユーザーがサインインし、 必要なら MFA を済ませ、スコープに同意します。
- サーバーがリダイレクトで戻す。 戻り先は登録済みの
redirect_uriと完全に一致する URI で、短命かつ 1 回限りのcodeと、同じstateが付きます。クライアントはstateが自分の送ったものと一致するかを確認し、一致しなければ処理を止めます。RFC 6749 はコードの有効期間を最長 10 分にすることを推奨しています。 - クライアントが
/tokenに POST する。 コード、リダイレクト URI、code_verifier(コンフィデンシャルクライアントならクライアント認証も)を送ります。これはリダイレクトではなく、バックチャネルのリクエストです。 - サーバーがトークンを返す。 アクセストークン、たいていはリフレッシュトークン、OpenID Connect を使うなら ID トークンです。
- クライアントが API を呼び出す。
Authorization: Bearer <access token>を付けます。 - API がトークンを検証して応答する。 JWT のアクセストークンなら、署名、発行者、オーディエンス、有効期限、スコープをローカルで確認します。不透明なトークンなら、認可サーバーのイントロスペクションエンドポイントに問い合わせます。
- アクセストークンの期限が切れたら、 クライアントはリフレッシュトークンを
/tokenに POST します。 - サーバーが新しいアクセストークンを返す。 パブリッククライアントには新しいリフレッシュトークンも返します。RFC 9700 は、パブリッククライアントのリフレッシュトークンを送信者に結び付けるか、ローテーションすることを求めています。そうすれば、盗まれたリフレッシュトークンも、どちらかが 1 回使った時点で機能しなくなります。
シーケンス図
ダイアグラムの内容
- User
- Client app
- Authorization server
- API (resource server)
- access token expired
- 1. Sign in
- 2. GET /authorize + code_challenge, state
- 3. Login and consent page
- 4. Credentials, MFA, consent
- 5. Redirect to redirect_uri with code, state
- 6. POST /token: code + code_verifier
- 7. Access token, refresh token
- 8. GET /orders, Authorization: Bearer token
- 9. 200 OK
- 10. POST /token: refresh_token
- 11. New access token (and rotated refresh token)
図の読み方は次のとおりです。
- 4 本のライフライン: 左から、ユーザー、クライアントアプリ、認可サーバー、API です。
- 塗りつぶしの矢じりが付いた実線の矢印 がリクエストで、破線の矢印 がレスポンスと戻りのリダイレクトです。
- ステップ 2〜5 はブラウザーを通るフロントチャネルのリダイレクトで、ステップ 6 は PKCE が効果を発揮するバックチャネルのトークンリクエストです。
- 下部の opt フラグメント は、アクセストークンの期限が切れたときだけ実行されます。
- Markdown ブロック には各当事者が行う確認を並べています。実際のバグの多くはここに潜んでいます。
レビューで見つかる間違い
stateを確認しない。 確認がないと、攻撃者は被害者のブラウザーで、攻撃者自身のアカウントによるログインフローを完了させられます。- リダイレクト URI のゆるい照合。 サーバーは登録済みの URI と完全一致で比較しなければなりません。前方一致やワイルドカードでの照合は、コードの窃取を許します。
- シングルページアプリのローカルストレージにトークンを置く。 スクリプトが注入されれば読み取られてしまいます。多くのチームは SPA の前に小さなバックエンド(Backend for Frontend)を置き、そこでトークンを保持して、ブラウザーにはセッション Cookie を渡しています。
- 有効なトークンなら何でも受け入れる API。 オーディエンスを確認しないと、別の API 向けに発行されたトークンが自分の API でも通ってしまいます。
- モバイルアプリに埋め込んだ Web ビュー。 RFC 8252 は、ネイティブアプリはシステムのブラウザーを使うべきだとしています。そうすればアプリはユーザーの認証情報を見られません。
- リフレッシュの処理を避けるための長寿命なアクセストークン。 アクセストークンは短くしてリフレッシュします。そうすれば失効がすぐに効きます。
OAuth の図に描くべきこと
- 各当事者をそれぞれのライフラインとして描く。 クライアントにバックエンドがあるならブラウザーも含めます。
- フロントチャネルとバックチャネルを区別する。 セキュリティ上の性質が異なるからです。
- 矢印にパラメーター名を書く:
state、code_challenge、code、code_verifier。レビュアーはこれらを探します。 - 誰が何を検証するか。 確認こそが設計であり、矢印はデータを動かしているだけです。
より広いセキュリティ全体を描くなら、ゼロトラストのアーキテクチャ図 で ID プロバイダーがほかの制御の間のどこに位置するかを、API ゲートウェイのアーキテクチャ図 でエッジで一度だけトークンを検証する方法を扱っています。ここで使った記法については、シーケンス図の例 で詳しく説明しています。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- シーケンスパックから、ユーザー用に アクター を、クライアントアプリ、認可サーバー、API 用に ライフライン ノードを 3 つ追加し、同じ高さで 1 列に並べます。
- 各メッセージをライフライン間の直線のエッジとして上から下へ描き、ラベルにパラメーター名を書きます。レスポンスとリダイレクトには、開いた矢じりの破線のエッジを使います。
- リフレッシュのステップを囲むように、
opt演算子の 複合フラグメント を追加します。 - 各当事者の確認事項を書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 6 個で、Free プランの範囲に収まります。
よくある質問
OAuth 2.0 と OpenID Connect の違いは何ですか? OAuth 2.0 は委任されたアクセスの仕組みで、クライアントは API を呼び出すためのトークンを受け取ります。OpenID Connect はその上に ID を加えます。ユーザーが誰かをクライアントに伝える ID トークンと、標準のユーザー情報エンドポイントです。
アプリにクライアントシークレットがあっても PKCE は必要ですか? RFC 9700 はコンフィデンシャルクライアントにも推奨しています。コードインジェクションを防げて、コストはそれぞれの側でパラメーターが 1 つ増えるだけです。
インプリシットグラントはまだ使うべきですか? いいえ。現在のベストプラクティスは、クライアントはそれを使うべきではないとしています。シングルページアプリでも、代わりに PKCE 付きの認可コードグラントを使います。
シングルページアプリはトークンをどこに保存すべきですか? できればブラウザーには一切置かないことです。Backend for Frontend がサーバー側でトークンを保持し、ブラウザーには安全なセッション Cookie を渡せます。どうしてもブラウザーに置く場合は、メモリーに保持し、アクセストークンを短くして、リフレッシュトークンのローテーションを使います。
API はアクセストークンをどう検証しますか? JWT のアクセストークンなら、認可サーバーが公開している鍵で署名を確認し、あわせて発行者、オーディエンス、有効期限、スコープを確認します。不透明なトークンなら、イントロスペクションエンドポイントを呼び出します。