OAuth 2.0 のフロー 認可コード、PKCE、およびクライアント資格情報
OAuth 2.0の適切なフローを選択する開発者のガイド。認可コード(ウェブアプリ)、PKCE(SPAsおよびモバイルアプリ)、クライアント資格(サーバー間)について詳しく説明し、実際のコード例と後でバイトする間違いを紹介。
3つのフローが実際のOAuth 2.0使用ケースの95%をカバーしています。仕様ではさらに詳細が記載されていますが、残りは廃止されている、または例外的なケース、または両方です。最初に正しいものを選ぶことで、認可要件が変更されたときに苦痛なリファクタリングを回避できます。
| フロー | 使用するタイミング | ユーザーが関与するか? | クライアントシークレットが必要か? |
|---|---|---|---|
| 認可コード | バックエンドサーバーを持つウェブアプリ | はい | はい — サーバーに残ります |
| 認可コード + PKCE | SPA、モバイルアプリ、または公開クライアント | はい | いいえ |
| クライアント資格情報 | サーバー間(ユーザーなし) | いいえ | はい — サーバーに残ります |
認可コードフロー
ウェブアプリとバックエンドの標準フローです。アクセストークンおよびクライアントシークレットはブラウザに触れません — トークン交換はサーバー側で行われます。これが全体の目的です。
ステップ1: ユーザーをリダイレクト
認可URLを構築し、ブラウザにリダイレクトします:
GET https://accounts.example.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid+profile+email
&state=RANDOM_CSRF_TOKEN
の state 値はリダイレクトのためのCSRF防御です。各フローごとに新しいランダム文字列を生成し、セッションに保存し、ユーザーが戻ってきたときに検証します。このチェックをスキップすると、攻撃者は自らの認可コードを使ってユーザーをフローを通過させ、ユーザーのアカウントを攻撃者の識別情報に無意識にリンクさせることができます。
ステップ2: コールバックを処理
認可サーバーはあなたの redirect_uri にリダイレクトします:
GET https://yourapp.com/callback?code=AUTH_CODE&state=SAME_STATE_YOU_SENT
検証 state が保存されたものと一致する場合、サーバー側でコードを交換します。
ステップ3: コードをトークンに交換(サーバー側のみ)
POST https://accounts.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
応答:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200a12b3c...",
"scope": "openid profile email"
}
両方のトークンをサーバー側に保持します。そして access_token が期限切れ( expires_in)の場合、 refresh_token を使って新しいトークンを取得し、ユーザーに再ログインを要求せずに済みます。
PKCE — シークレットを保持できないクライアント向け
SPAまたはモバイルアプリは、安全な場所にシークレットを保持できません。 client_secret誰もが開発ツールを開いて、あなたのJSバンドル内に見つけることができます。誰もがAPKをデコンパイルできます。PKCE(コード交換の証明キー、発音「pixy」)は、共有シークレットを必要としない1回限りの暗号化チャレンジでこれを解決します。
フローは認可コードとまったく同じですが、2つの追加項目があります: code_verifier (ランダムな文字列を生成したもの) と code_challenge (バリファイアのSHA-256ハッシュ、base64urlエンコード済み)。あなたはチャレンジを事前に送信し、交換時にバリファイアを持っていることを証明します。
ステップ1: バリファイアとチャレンジを生成
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64url(array);
}
async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64url(new Uint8Array(digest));
}
function base64url(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier in memory — NOT localStorage
をメモリに保存します — モジュールスコープの変数、localStorageではなく。トークン交換時にそれを送信します。 code_verifier ステップ2: 認可リクエスト — チャレンジを追加
ステップ3: トークン交換 — バリファイアではなくクライアントシークレット
GET https://accounts.example.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid+profile
&state=RANDOM_CSRF_TOKEN
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
認可サーバーはバリファイアをハッシュ化し、ステップ2で送信したチャレンジと比較します。認可コードをキャプチャした攻撃者はバリファイアの内容を知りません — それを使って交換することはできません。
POST https://accounts.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&code_verifier=ORIGINAL_VERIFIER_YOU_GENERATED
重要な点:OAuth 2.1(OAuth 2.0の進行中の現代化)は、リダイレクトに関連するすべてのフローに対してPKCEを義務付けています。新しいコードを書いている場合、現在のプロバイダーがそれを必要としていない場合でも、PKCEを使用すべきです。
クライアント資格情報 — ユーザーなしでも問題ありません
バックグラウンドジョブ、マイクロサービスが他のマイクロサービスに呼び出したり、クロントスクールがAPIにアクセスするなど、これらはすべてユーザーを含みません。クライアント資格情報が正しいフローです:サービスは自らのクライアントIDとシークレットを使って直接認証します。
応答:
POST https://accounts.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=api:read api:write
リフレッシュトークンなし — 期限切れになったら新しいものを要求します。一般的な誤り:API呼び出しのたびにトークンエンドポイントにアクセスすることです。トークンをキャッシュし、各リクエスト前に期限を確認し、期限が近づいたときにのみ再取得します。1日(または1時間、
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 86400
}
)ごとに1回のトークンリクエスト(または expires_in)ではなく、1リクエストごとに1回のトークンリクエスト:
let cachedToken = null;
let tokenExpiresAt = 0;
async function getAccessToken() {
// Refresh 30 seconds before actual expiry
if (cachedToken && Date.now() < tokenExpiresAt - 30_000) {
return cachedToken;
}
const res = await fetch('https://accounts.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: 'api:read api:write',
}),
});
const data = await res.json();
cachedToken = data.access_token;
tokenExpiresAt = Date.now() + (data.expires_in * 1000);
return cachedToken;
}
開発者が実際に犯す誤り
トークンをlocalStorageに保存する
XSS脆弱性 — 自分のコード、依存関係、タグマネージャーのスクリプトなど、すべての場所でlocalStorageに含まれる内容を読み取ることができます。SPAの場合:アクセストークンをメモリに保存(モジュールスコープの変数で、リフレッシュ時に消えます)。バックエンドがそれを設定できる場合、リフレッシュトークンをhttpOnlyクッキーとして保存します。JavaScriptはhttpOnlyクッキーを読み取れません。
暗黙フローを使用する
暗黙フローはトークンをURLフラグメント(#access_token=...)に直接返します。これらのトークンはブラウザの履歴、サーバーアクセスログ、リファラーヘッダーに残ります。RFC 9700で廃止されています。新しいコードでは暗黙フローを使用する理由はありません。PKCEを使用してください。
ステート検証をスキップする
.yml としてダウンロード state コールバックでの検証をスキップすると、攻撃者は認可コードを使って自らの認可コードでOAuthフローを完了できるリダイレクトURLを作成できます。結果として、ユーザーのアカウントがプロバイダー側で攻撃者の識別情報にリンクされます。各フローごとに新しいものを生成し、セッションに保存し、コールバック時に検証してください。
クライアントシークレットをフロントエンドコードに置く
ブラウザに存在するシークレットは存在しません。最小化はそれを隠すことはできません。暗号化はそれを保護することはできません。あなたのランタイムがブラウザまたはモバイルアプリの場合、あなたは 公開クライアント — PKCEを使用し、クライアントシークレットを完全に省略します。これはワークアラウンドではなく、仕様が公開クライアントがどのように動作するべきであると意図しているものです。
トークンの期限切れを積極的に処理しない
すべてのアクセストークンには expires_in 値があります。トークンを期限切れまで保持し、401エラーで失敗した後に再認証すると、ユーザーが不思議なエラーに直面します。リクエスト前に期限を確認し、リフレッシュを積極的に(期限30秒前が適切なバッファ)し、リフレッシュトークン自体が期限切れになった場合の稀なケースも処理します。
フローをデバッグする際にトークンを確認する
ほとんどのOAuthプロバイダーはアクセストークンとしてJWTを発行します。ペイロードはbase64urlエンコードされており、プライベートキーなしで読み取れます — しかしこの署名にはキーが必要です。デバッグ中にトークンのクレーム、スコープ、または期限を確認したい場合、それを JWTデコーダー.
に貼り付けます。 code_challenge PKCEを手動テストする場合に、base64urlエンコードされた がデコードされるか確認したい場合、 Base64変換器
短い要約
1つの質問が正しいフローを決定します:あなたのランタイムに安全なシークレットを保持できる場所がありますか?
- バックエンドサーバー → 認可コードまたはクライアント資格情報(シークレットはサーバーに残る)
- ブラウザまたはモバイルアプリ → 認可コード + PKCE(シークレットはまったくない)
- ユーザーが関与しない → クライアント資格情報
PKCEはリダイレクトに関与するすべてのフローに適用され、プロバイダーがまだ要望していない場合でも、使用するべきです。OAuth 2.1ではこれに従います。
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
