mcpField
O
oauth-implementation

oauth-implementation

github.com/mindrallyセキュリティ287725 回インストール3 週間前に更新
リポジトリ

oauth-implementation について

oauth-implementation は、OAuth 2.0 と 2.1 の認証フロー実装向けスキルです。アプリケーション開発者に、PKCE を使う認可、トークン処理、セキュリティ確認の指針を示します。

主な機能

  1. 01OAuth 2.1 フローの指針
  2. 02PKCE と state の検証
  3. 03トークン検証とローテーション
  4. 04リダイレクト URI とスコープの確認
  5. 05セキュリティテストのチェックリスト

活用シーン

  1. 01認可コードフローの実装
  2. 02OAuth コールバックの CSRF 対策
  3. 03アクセストークンとリフレッシュトークンの検証
  4. 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 が検証される
  • エラーケースが適切に処理される

コメント