oauth-implementation について
oauth-implementation は、OAuth 2.0 と 2.1 の認証フロー実装向けスキルです。アプリケーション開発者に、PKCE を使う認可、トークン処理、セキュリティ確認の指針を示します。
主な機能
- 01OAuth 2.1 フローの指針
- 02PKCE と state の検証
- 03トークン検証とローテーション
- 04リダイレクト URI とスコープの確認
- 05セキュリティテストのチェックリスト
活用シーン
- 01認可コードフローの実装
- 02OAuth コールバックの CSRF 対策
- 03アクセストークンとリフレッシュトークンの検証
- 04リダイレクトとスコープ制御の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill oauth-implementationインストール後、Claude Code で/oauth-implementationを入力して読み込まれたか確認します。
詳細
OAuth の実装
OAuth 2.0 および OAuth 2.1 の実装に関する専門知識を備えています。OAuth 認証フローを実装する際は、以下のガイドラインに従ってください。
基本原則
- OAuth 2.1 のパターンを常に使用します(PKCE 必須、インプリシットフローは使用しません)
- すべての OAuth 通信に HTTPS を使用します
- CSRF 保護のために適切な状態管理を実装します
- スコープには最小権限の原則を適用します
- すべてのトークンをサーバー側で検証します
OAuth 2.1 の主な要件
OAuth 2.1 ではベストプラクティスが統合され、安全でないパターンが廃止されています。
- 認可コードフローを使用するすべてのクライアントで PKCE が必須です
- インプリシットグラントは削除されています
- リソースオーナーパスワードクレデンシャルグラントは削除されています
- リダイレクト URI は完全一致で照合する必要があります
- リフレッシュトークンは送信者制約を適用するか、ローテーションを使用する必要があります
PKCE を使用した認可コードフロー
ステップ 1:PKCE パラメーターの生成
// Generate cryptographically secure code verifier
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
// Create code challenge from verifier
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(digest));
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
ステップ 2:認可リクエスト
async function initiateOAuthFlow() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = generateSecureRandomString();
// Store for later verification
sessionStorage.setItem('oauth_code_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
window.location.href = `${AUTHORIZATION_ENDPOINT}?${params}`;
}
ステップ 3:コールバックの処理とトークン交換
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const error = params.get('error');
// Check for errors
if (error) {
throw new Error(`OAuth error: ${error} - ${params.get('error_description')}`);
}
// Validate state to prevent CSRF
const storedState = sessionStorage.getItem('oauth_state');
if (state !== storedState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
// Retrieve code verifier
const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
// Exchange code for tokens
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
// Clean up
sessionStorage.removeItem('oauth_code_verifier');
sessionStorage.removeItem('oauth_state');
return tokens;
}
サーバー側の実装
コンフィデンシャルクライアントのトークン交換
// Node.js/Express example
app.post('/oauth/callback', async (req, res) => {
const { code, state } = req.body;
// Validate state
if (state !== req.session.oauthState) {
return res.status(400).json({ error: 'Invalid state' });
}
try {
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// Client authentication for confidential clients
Authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
code_verifier: req.session.codeVerifier,
}),
});
const tokens = await tokenResponse.json();
// Store tokens securely server-side
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
res.redirect('/dashboard');
} catch (error) {
res.status(500).json({ error: 'Token exchange failed' });
}
});
トークンセキュリティのベストプラクティス
アクセストークンの検証
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: `${ISSUER}/.well-known/jwks.json`,
cache: true,
cacheMaxAge: 600000, // 10 minutes
});
function getSigningKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
async function validateToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getSigningKey,
{
audience: EXPECTED_AUDIENCE,
issuer: EXPECTED_ISSUER,
algorithms: ['RS256'], // Whitelist allowed algorithms
},
(err, decoded) => {
if (err) reject(err);
else resolve(decoded);
}
);
});
}
リフレッシュトークンのローテーション
async function refreshAccessToken(refreshToken) {
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
}),
});
if (!response.ok) {
// Refresh token may be expired or revoked
throw new Error('Refresh token invalid');
}
const tokens = await response.json();
// If rotation is enabled, you'll receive a new refresh token
// Store the new refresh token and invalidate the old one
return tokens;
}
セキュリティ要件
リダイレクト URI の検証
// Server-side: validate redirect URIs against whitelist
const ALLOWED_REDIRECT_URIS = [
'https://myapp.com/callback',
'https://myapp.com/oauth/callback',
];
function validateRedirectUri(uri) {
// Exact string matching - no wildcards
return ALLOWED_REDIRECT_URIS.includes(uri);
}
スコープの管理
// Request minimum necessary scopes
const SCOPES = {
basic: 'openid profile email',
readOnly: 'openid profile email read:data',
fullAccess: 'openid profile email read:data write:data',
};
// Validate scopes on the server
function validateScopes(requestedScopes, allowedScopes) {
const requested = requestedScopes.split(' ');
const allowed = allowedScopes.split(' ');
return requested.every(scope => allowed.includes(scope));
}
防止すべき一般的な脆弱性
1. 認可コードインジェクション
常に PKCE を使用してください。code_verifier により、元のリクエスト元だけがコードを交換できます。
2. CSRF 攻撃
// Always use and validate the state parameter
const state = crypto.randomBytes(32).toString('hex');
// Store in session and validate on callback
3. オープンリダイレクト
// Never construct redirect URIs from user input
// Always use whitelisted URIs
const redirectUri = ALLOWED_REDIRECT_URIS[0]; // Don't: req.query.redirect_uri
4. トークン漏えい
// Never log tokens
console.log('User authenticated'); // Good
console.log(`Token: ${accessToken}`); // NEVER DO THIS
// Don't include tokens in URLs
// Use Authorization header instead
fetch('/api/resource', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
トークン保存に関する推奨事項
ブラウザーアプリケーション
// Option 1: Memory (most secure, but lost on refresh)
let accessToken = null;
// Option 2: HttpOnly cookies (requires backend)
// Set by server with appropriate flags
// Secure, HttpOnly, SameSite=Strict
// Option 3: sessionStorage (cleared when tab closes)
sessionStorage.setItem('access_token', token);
// Avoid localStorage for sensitive tokens
// Vulnerable to XSS attacks
サーバーアプリケーション
// Store tokens encrypted in session or database
const encryptedToken = encrypt(accessToken, SESSION_ENCRYPTION_KEY);
req.session.encryptedAccessToken = encryptedToken;
エラー処理
const OAUTH_ERRORS = {
invalid_request: 'The request is missing a required parameter',
unauthorized_client: 'The client is not authorized',
access_denied: 'The user denied the request',
unsupported_response_type: 'The response type is not supported',
invalid_scope: 'The requested scope is invalid',
server_error: 'The authorization server encountered an error',
temporarily_unavailable: 'The server is temporarily unavailable',
};
function handleOAuthError(error, errorDescription) {
const message = OAUTH_ERRORS[error] || 'Unknown error';
console.error(`OAuth Error: ${message}. Details: ${errorDescription}`);
// Show user-friendly error message
}
テストチェックリスト
- PKCE フローが正しく動作する
- state パラメーターが検証される
- 無効な state が拒否される
- トークンの有効期限が適切に処理される
- リフレッシュトークンのローテーションが機能する
- 無効なトークンが拒否される
- スコープが適切に適用される
- リダイレクト URI が検証される
- エラーケースが適切に処理される
コメント
Markdown 対応