OAuth 2.0、JWT、APIキー管理など、APIの認証・認可に関する実践的なガイド。
OAuth 2.0はAPIアクセスの認可フレームワークとして最も広く使われています。適切なグラントタイプの選択が重要です。
| グラントタイプ | ユースケース | セキュリティレベル |
|---|---|---|
| Authorization Code + PKCE | SPAやモバイルアプリ(推奨) | 最高 |
| Authorization Code | サーバーサイドWebアプリ | 高 |
| Client Credentials | サーバー間通信(M2M) | 中 |
| Implicit(非推奨) | レガシーSPA | 低 |
| Resource Owner Password(非推奨) | 高信頼ファーストパーティのみ | 低 |
Implicit GrantはトークンがURLフラグメントに露出するため、現在は非推奨です。SPAでは Authorization Code + PKCE を使用してください。
// 1. code_verifier をランダム生成 function generateCodeVerifier() { const array = new Uint8Array(32); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 2. code_challenge を SHA-256 で生成 async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 3. 認可リクエスト const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); const authUrl = `https://auth.example.com/authorize?` + `response_type=code&` + `client_id=${clientId}&` + `redirect_uri=${redirectUri}&` + `code_challenge=${challenge}&` + `code_challenge_method=S256&` + `scope=read write`;
JWTはステートレスな認証トークンとして広く使われますが、実装ミスがセキュリティホールに直結します。
const jwt = require('jsonwebtoken'); // --- トークン発行 --- function issueTokens(user) { const accessToken = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '15m', issuer: 'api.example.com', audience: 'app.example.com', } ); const refreshToken = jwt.sign( { sub: user.id, type: 'refresh' }, process.env.REFRESH_SECRET, { algorithm: 'HS256', expiresIn: '7d' } ); return { accessToken, refreshToken }; } // --- トークン検証 --- function verifyAccessToken(token) { return jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'], // ← 必ず指定! issuer: 'api.example.com', audience: 'app.example.com', }); }
| 保存場所 | XSS耐性 | CSRF耐性 | 推奨度 |
|---|---|---|---|
| HttpOnly Cookie | ◎ | △(SameSite設定要) | 推奨 |
| メモリ(変数) | ◎ | ◎ | ページ遷移で消失 |
| localStorage | ✕(XSSで窃取可能) | ◎ | 非推奨 |
| sessionStorage | ✕(XSSで窃取可能) | ◎ | 限定的 |
アクセストークンは HttpOnly、Secure、SameSite=Strict 属性を付けた Cookie に保存するのが最も安全です。これにより XSS 攻撃でのトークン窃取を防ぎます。
APIキーはシンプルな認証手段ですが、適切に管理しないと重大なリスクになります。
十分なエントロピーで生成(256bit以上)。ハッシュ化して保存し、プレーンテキストでDBに保持しない。
キーごとに最小限のスコープ(読み取り専用、特定リソースのみ等)を付与。ワイルドカード権限は避ける。
定期的なキーローテーション(90日以内)。古いキーのグレースピリオド設定。漏洩時は即時無効化。
ヘッダー(X-API-Key or Authorization)で送信。URLクエリパラメータには含めない(ログに残る)。
キーはソースコードにハードコードせず、環境変数やシークレットマネージャーで管理。
APIキーの使用状況を監視。異常なパターン(大量リクエスト、未知のIPからのアクセス)を検出。
const crypto = require('crypto'); // APIキーをSHA-256でハッシュ化して比較 async function validateApiKey(req, res, next) { const apiKey = req.headers['x-api-key']; if (!apiKey) { return res.status(401).json({ error: 'API key required' }); } // ハッシュ化してDB照合(タイミング攻撃対策のため timingSafeEqual は不要 - ハッシュ比較のため) const hashedKey = crypto .createHash('sha256') .update(apiKey) .digest('hex'); const keyRecord = await db.findApiKey(hashedKey); if (!keyRecord || keyRecord.revokedAt) { return res.status(403).json({ error: 'Invalid API key' }); } // 有効期限チェック if (keyRecord.expiresAt && new Date() > keyRecord.expiresAt) { return res.status(403).json({ error: 'API key expired' }); } req.apiClient = keyRecord; next(); }
AIエージェントやLLMを活用したアプリケーションには、従来のAPIセキュリティに加えて、追加の認証・認可パターンが必要です。
モデルのティアごとに異なるアクセスレベルを割り当てます(例: GPT-4は上位スコープが必要)。高コストまたは機密性の高いモデルの不正利用を防止します。
アクセストークンやAPIキーのメタデータにトークン予算を埋め込みます。リクエスト単位・日単位の制限を適用し、Denial-of-Wallet攻撃を防止します。
特定のプロンプトタイプ(例: コード生成、データ分析)の実行前にユーザー権限を確認します。プロンプトカテゴリをロールベースの権限にマッピングします。
会話履歴、ファインチューニング済みモデル、RAGデータがテナントごとに分離されていることを保証します。名前空間の適用を伴うテナントスコープのAPIキーを使用します。
AIエージェントに暗号学的なIDを割り当てます。エージェント間通信には署名付きJWTを使用します。ツールアクセスの付与前にエージェントのIDを検証します。
LLMプロバイダーのAPIキーローテーションを自動化します。エージェントセッションには短命トークンを使用します。エージェントの廃止時にはクレデンシャルを即時失効させます。
| シナリオ | 認証方式 | 必要なスコープ | 承認 |
|---|---|---|---|
| エージェントが公開データを読み取り | API Key | data:read |
自動 |
| エージェントがユーザーデータを変更 | OAuth 2.0 (delegated) | data:write |
ユーザーの同意が必要 |
| エージェントが外部APIを呼び出し | OAuth 2.0 + mTLS | external:invoke |
Human-in-the-loop(人間による確認) |
| エージェントがコード/シェルを実行 | Scoped JWT + Sandbox | exec:sandbox |
管理者承認 + 監査ログ |
import jwt import time from datetime import datetime, timedelta class AgentTokenIssuer: """Issues scoped, short-lived tokens for AI agents.""" ALLOWED_SCOPES = { "reader": ["data:read"], "writer": ["data:read", "data:write"], "executor": ["data:read", "data:write", "exec:sandbox"], } def __init__(self, secret_key: str): self.secret_key = secret_key def issue_agent_token( self, agent_id: str, role: str, tenant_id: str, token_budget: int = 10000, ttl_minutes: int = 15, ) -> str: # ロールを検証しスコープを解決 if role not in self.ALLOWED_SCOPES: raise ValueError(f"Invalid role: {role}") payload = { "sub": agent_id, "tenant": tenant_id, "scopes": self.ALLOWED_SCOPES[role], "token_budget": token_budget, "iat": datetime.utcnow(), "exp": datetime.utcnow() + timedelta(minutes=ttl_minutes), "type": "agent", } return jwt.encode(payload, self.secret_key, algorithm="HS256") def verify_tool_access(self, token: str, required_scope: str) -> dict: # エージェントトークンをデコードして検証 payload = jwt.decode( token, self.secret_key, algorithms=["HS256"] ) if required_scope not in payload["scopes"]: raise PermissionError( f"Agent {payload['sub']} lacks scope: {required_scope}" ) return payload
| 方式 | ステートレス | 適用場面 | セキュリティ上の注意 |
|---|---|---|---|
| OAuth 2.0 + PKCE | ○ | ユーザー認可が必要なAPI | state/PKCE必須、トークン有効期限管理 |
| JWT Bearer | ○ | マイクロサービス間通信 | alg指定必須、ペイロードに機密情報を入れない |
| APIキー | ○ | サーバー間、外部連携 | ローテーション・スコープ制限必須 |
| mTLS | ○ | 高セキュリティM2M通信 | 証明書管理の運用コスト高 |
| セッションCookie | ✕ | 従来型Webアプリ | CSRF対策必須、スケーラビリティに課題 |