OAuth 2.0とOpenID Connectは、モダンなWeb認証・認可の基盤を形成しています。広く使われているにもかかわらず、これらのプロトコルは誤解され、誤設定されることが多く、予防可能なセキュリティ脆弱性を生み出しています。本ガイドでは、アプリケーションに安全な認証を統合するための基本概念、実装パターン、セキュリティベストプラクティスを解説します。
OAuth 2.0の基礎
OAuth 2.0は認可フレームワークであり、認証プロトコルではありません。この区別は極めて重要です。OAuthはクライアントアプリケーションが保護リソースへの委任アクセスを取得する方法を定義しますが、ユーザーIDの検証方法は規定しません。その役割を担うのがOpenID Connectです。
プロトコルは4つのロールを定義します:
| ロール | 説明 | 例 |
|---|---|---|
| リソースオーナー | アクセスを許可できるエンティティ | エンドユーザー |
| クライアント | アクセスを要求するアプリケーション | Webアプリ、モバイルアプリ |
| 認可サーバー | 認証成功後にトークンを発行 | Auth0、Keycloak |
| リソースサーバー | 保護リソースをホスト | APIサーバー |
認可コードフローとPKCE
PKCE(Proof Key for Code Exchange)付き認可コードフローは、ほとんどのアプリケーションで推奨されるグラントタイプです。PKCEは、暗号チャレンジを通じて認可リクエストをトークンリクエストにバインドすることで、認可コードインターセプト攻撃を防止します。
// Step 1: code_verifierとcode_challengeの生成
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
.createHash("sha256")
.update(verifier)
.digest("base64url");
return { verifier, challenge };
}
// Step 2: 認可サーバーにユーザーをリダイレクト
const { verifier, challenge } = generatePKCE();
const authUrl = new URL("https://authorization-server.com/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", CLIENT_ID);
authUrl.searchParams.set("redirect_uri", CALLBACK_URL);
authUrl.searchParams.set("code_challenge", challenge);
authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set("state", crypto.randomUUID());
// Step 3: 認可コードをトークンと交換
async function exchangeCode(code, verifier) {
const response = await fetch("https://authorization-server.com/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: CALLBACK_URL,
client_id: CLIENT_ID,
code_verifier: verifier,
}),
});
return response.json();
}
stateパラメータはCSRF対策を提供します。PKCEは現在ではサーバーサイドWebアプリケーションでも推奨されており、認可コードインターセプトに対する多層防御を提供します。
グラントタイプ比較
| グラントタイプ | ユースケース | セキュリティ特性 |
|---|---|---|
| 認可コード + PKCE | Web、モバイル、SPA | 最強 — 新規アプリに推奨 |
| クライアントクレデンシャル | サーバー間、サービスアカウント | ユーザーコンテキストなし |
| デバイス認可 | CLI、IoT、スマートTV | 別デバイスでのURLアクセスが必要 |
| 認可コード(PKCEなし) | レガシーサーバーサイドアプリ | コードインターセプト脆弱性あり |
| 暗黙的(非推奨) | レガシーSPA | 仕様から削除済み |
OpenID Connectレイヤー
OpenID Connect(OIDC)は、OAuth 2.0の上にアイデンティティレイヤーを追加します。鍵となる追加要素はid_tokenで、認証されたユーザーに関する検証済みクレームを含むJWTです。
// Node.jsでのIDトークン検証
const jwksClient = require("jwks-rsa");
const jwt = require("jsonwebtoken");
const client = jwksClient({
jwksUri: "https://authorization-server.com/.well-known/jwks.json",
});
async function verifyIdToken(idToken) {
const decoded = jwt.decode(idToken, { complete: true });
const key = await client.getSigningKey(decoded.header.kid);
const signingKey = key.getPublicKey();
return jwt.verify(idToken, signingKey, {
issuer: "https://authorization-server.com",
audience: CLIENT_ID,
algorithms: ["RS256"],
});
}
標準的なIDトークンクレームには、sub(サブジェクト識別子)、iss(発行者)、aud(オーディエンス)、exp(有効期限)、iat(発行日時)、nonce(リプレイ防止)が含まれます。
IDトークンとアクセストークンの違い
IDトークンとアクセストークンの違いは、しばしば混乱の原因となります。IDトークンは認証用です — クライアントにユーザーが誰かを伝えます。アクセストークンは認可用です — クライアントが何を許可されているかをリソースサーバーに伝えます。
IDトークンをAPI認可に使用してはいけません。アクセストークンは、不透明(リソースサーバーがイントロスペクションで検証するランダム文字列)またはJWT(クレームを内包する自己完結型)の形式をとります。
リフレッシュトークンローテーション
リフレッシュトークンローテーションは、リフレッシュ操作ごとに新しいリフレッシュトークンを発行し、古いトークンを無効化するセキュリティメカニズムです。
async function refreshAccessToken(refreshToken) {
const response = await fetch("https://authorization-server.com/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID,
}),
});
const tokens = await response.json();
// 古いリフレッシュトークンは無効化される
await secureStore.save("refresh_token", tokens.refresh_token);
return tokens.access_token;
}
Auth0、Okta、Keycloakなどの主要プロバイダーは、デフォルトでリフレッシュトークンローテーションを有効にしています。再利用検出(ローテートされたトークンが再使用された場合に全トークンを無効化)と組み合わせることで、トークン盗難に対する強力な保護を提供します。
セキュリティベストプラクティス
トークンの保存方法は最も重要なセキュリティ判断のひとつです。ブラウザベースのアプリケーションでは、アクセストークンをメモリに保存し、リフレッシュトークンにhttpOnly Cookieを使用します。localStorageは同じオリジンのすべてのJavaScriptからアクセス可能なため、トークンの保存に使用してはいけません。
| 保存方法 | アクセストークン | リフレッシュトークン |
|---|---|---|
| メモリ変数 | SPAに最適(永続化なし) | 不適切 |
| httpOnly Cookie | 非推奨 | 最適 — XSSから保護 |
| localStorage | XSS脆弱性あり | XSS脆弱性あり |
常にリダイレクトURIをサーバーサイドで検証し、短命なアクセストークン(15〜60分)を使用し、リクエストするスコープを機能に必要な最小限に制限してください。
結論
OAuth 2.0とOpenID Connectは強力ですが、微妙なニュアンスを持つプロトコルです。すべての新規アプリケーションにはPKCE付き認可コードフローを使用し、クライアント側でIDトークンを徹底的に検証し、リフレッシュトークンローテーションを実装し、localStorageにトークンを保存しないでください。これらのパターンに従い、各グラントタイプのセキュリティ特性を理解することで、ユーザーフレンドリーで一般的な攻撃に耐性のある認証システムを構築できます。
