mcpField
O
oauth-oidc-implementer

oauth-oidc-implementer

github.com/curiositechセキュリティ257171 回インストール2 日前に更新
リポジトリ

oauth-oidc-implementer について

oauth-oidc-implementerは、OAuth 2.0とOpenID Connect認証を実装する開発者向けのスキルです。一般的な認可フロー、トークン管理、IDプロバイダー設定に加え、クライアント側とサーバー側のセキュリティ確認を扱います。

主な機能

  1. 01OAuth 2.0フローの実装
  2. 02OIDCクレームとディスカバリー
  3. 03JWT検証とトークン管理
  4. 04更新・失効・ログアウトのパターン
  5. 05PKCE・state・nonceによる保護
  6. 06IDプロバイダーの設定

活用シーン

  1. 01アプリへのソーシャルログイン追加
  2. 02OAuth 2.0によるAPIリクエストの認可
  3. 03OIDCによる企業SSOの設定
  4. 04トークン検証や更新の問題調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill oauth-oidc-implementer

インストール後、Claude Code で/oauth-oidc-implementerを入力して読み込まれたか確認します。

同梱ファイル.claude-plugin/plugin.json

詳細

OAuth/OIDC 実装者

概要

OAuth 2.0 と OpenID Connect(OIDC)の認証フローの実装に精通しています。セキュアなトークン管理、ソーシャルログインの統合、API 認可、ID プロバイダーの設定を専門としています。セキュリティのベストプラクティスに沿って、クライアント側とサーバー側の両方のフローに対応します。

使用する場面

  • 「Google/GitHub などでログイン」のソーシャルログインを実装する
  • API 認可のために OAuth 2.0 を設定する
  • エンタープライズ SSO のために OIDC を設定する
  • トークンの更新とセッション管理を設計する
  • モバイルアプリ/SPA 向けに PKCE を実装する
  • JWT 検証で API エンドポイントを保護する
  • ID プロバイダー(Auth0、Okta、Keycloak)と統合する
  • OAuth フローの失敗をトラブルシューティングする

機能

OAuth 2.0 フロー
  • Authorization Code フロー(PKCE 対応)
  • サービス間連携向けの Client Credentials フロー
  • Implicit フロー(レガシー。避けるべき理由も把握しています)
  • IoT 向けの Device Authorization フロー
  • リフレッシュトークンのローテーション
OpenID Connect
  • ID トークンの検証とクレーム
  • UserInfo エンドポイントの利用
  • Discovery ドキュメント(.well-known/openid-configuration)
  • セッション管理(フロントチャネル/バックチャネルログアウト)
  • パブリッククライアント向けの PKCE
トークン管理
  • JWT の構造(ヘッダー、ペイロード、署名)
  • アクセストークンと ID トークン、リフレッシュトークンの違い
  • トークンの保存方法(httpOnly Cookie と localStorage の比較)
  • トークン更新のパターン
  • 失効とログアウト
セキュリティのベストプラクティス
  • PKCE(Proof Key for Code Exchange)
  • CSRF 保護のための state パラメーター
  • リプレイ攻撃対策の nonce
  • セキュアなトークン保存
  • 有効期間の短いアクセストークン
ID プロバイダー
  • Auth0 の統合
  • Okta の設定
  • セルフホスト可能な Keycloak
  • Google/GitHub/Microsoft のソーシャルログイン
  • SAML と OIDC のブリッジ

依存関係

以下と組み合わせると効果的です:

  • nextjs-app-router-expert - フルスタック認証の実装
  • api-architect - API 認可の設計
  • cloudflare-worker-dev - エッジ認証
  • site-reliability-engineer - 認証のモニタリング

例

PKCE を使った Authorization Code フロー(SPA)
// 1. Generate PKCE challenge
function generatePKCE() {
  const verifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));
  const challenge = base64URLEncode(
    await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
  );
  return { verifier, challenge };
}

// 2. Redirect to authorization endpoint
function initiateLogin() {
  const { verifier, challenge } = generatePKCE();
  const state = crypto.randomUUID();

  // Store verifier and state for later validation
  sessionStorage.setItem('pkce_verifier', verifier);
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'your-client-id',
    redirect_uri: 'https://yourapp.com/callback',
    scope: 'openid profile email',
    state: state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  });

  window.location.href = `https://auth.example.com/authorize?${params}`;
}

// 3. Handle callback and exchange code for tokens
async function handleCallback(code: string, state: string) {
  // Validate state
  if (state !== sessionStorage.getItem('oauth_state')) {
    throw new Error('State mismatch - possible CSRF attack');
  }

  const verifier = sessionStorage.getItem('pkce_verifier');

  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'https://yourapp.com/callback',
      client_id: 'your-client-id',
      code_verifier: verifier,
    }),
  });

  const tokens = await response.json();
  // { access_token, id_token, refresh_token, expires_in }

  // Clean up
  sessionStorage.removeItem('pkce_verifier');
  sessionStorage.removeItem('oauth_state');

  return tokens;
}
Next.js API Route のトークンハンドラー
// app/api/auth/callback/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const state = searchParams.get('state');

  // Validate state from cookie
  const cookieStore = cookies();
  const storedState = cookieStore.get('oauth_state')?.value;

  if (state !== storedState) {
    return NextResponse.redirect('/auth/error?reason=state_mismatch');
  }

  // Exchange code for tokens (server-side, can use client_secret)
  const tokenResponse = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code!,
      redirect_uri: process.env.REDIRECT_URI!,
      client_id: process.env.CLIENT_ID!,
      client_secret: process.env.CLIENT_SECRET!,
    }),
  });

  const tokens = await tokenResponse.json();

  // Store refresh token in httpOnly cookie
  const response = NextResponse.redirect('/dashboard');

  response.cookies.set('refresh_token', tokens.refresh_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 30, // 30 days
    path: '/',
  });

  // Access token can go to client or httpOnly cookie
  response.cookies.set('access_token', tokens.access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: tokens.expires_in,
    path: '/',
  });

  return response;
}
JWT 検証(Node.js)
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  rateLimit: true,
});

function getKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) {
  client.getSigningKey(header.kid, (err, key) => {
    const signingKey = key?.getPublicKey();
    callback(err, signingKey);
  });
}

export async function validateToken(token: string): Promise<JWTPayload> {
  return new Promise((resolve, reject) => {
    jwt.verify(
      token,
      getKey,
      {
        algorithms: ['RS256'],
        issuer: 'https://auth.example.com/',
        audience: 'your-client-id',
      },
      (err, decoded) => {
        if (err) reject(err);
        else resolve(decoded as JWTPayload);
      }
    );
  });
}

// Middleware usage
export async function authMiddleware(req: Request) {
  const token = req.headers.get('Authorization')?.replace('Bearer ', '');

  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }

  try {
    const payload = await validateToken(token);
    // Attach user to request context
    return { user: payload };
  } catch (error) {
    return new Response('Invalid token', { status: 401 });
  }
}
トークン更新パターン
// Client-side token refresh with race condition handling
let refreshPromise: Promise<string> | null = null;

async function getAccessToken(): Promise<string> {
  const accessToken = localStorage.getItem('access_token');
  const expiresAt = localStorage.getItem('token_expires_at');

  // Check if token is still valid (with 60s buffer)
  if (accessToken && expiresAt && Date.now() < parseInt(expiresAt) - 60000) {
    return accessToken;
  }

  // Deduplicate concurrent refresh requests
  if (refreshPromise) {
    return refreshPromise;
  }

  refreshPromise = refreshAccessToken();

  try {
    return await refreshPromise;
  } finally {
    refreshPromise = null;
  }
}

async function refreshAccessToken(): Promise<string> {
  const response = await fetch('/api/auth/refresh', {
    method: 'POST',
    credentials: 'include', // Send httpOnly refresh token cookie
  });

  if (!response.ok) {
    // Refresh failed, redirect to login
    window.location.href = '/login';
    throw new Error('Token refresh failed');
  }

  const { access_token, expires_in } = await response.json();

  localStorage.setItem('access_token', access_token);
  localStorage.setItem('token_expires_at', String(Date.now() + expires_in * 1000));

  return access_token;
}
ソーシャルログインの設定(Auth0)
// auth0.config.ts
export const auth0Config = {
  domain: process.env.AUTH0_DOMAIN!,
  clientId: process.env.AUTH0_CLIENT_ID!,
  clientSecret: process.env.AUTH0_CLIENT_SECRET!,
  redirectUri: process.env.AUTH0_REDIRECT_URI!,
  scope: 'openid profile email',
  audience: process.env.AUTH0_AUDIENCE, // For API access
};

// Login URL builder
export function getLoginUrl(connection?: string) {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: auth0Config.clientId,
    redirect_uri: auth0Config.redirectUri,
    scope: auth0Config.scope,
    state: generateState(),
    ...(auth0Config.audience && { audience: auth0Config.audience }),
    ...(connection && { connection }), // 'google-oauth2', 'github', etc.
  });

  return `https://${auth0Config.domain}/authorize?${params}`;
}
OIDC Discovery
// Fetch and cache OIDC configuration
interface OIDCConfig {
  authorization_endpoint: string;
  token_endpoint: string;
  userinfo_endpoint: string;
  jwks_uri: string;
  issuer: string;
}

let oidcConfig: OIDCConfig | null = null;

export async function getOIDCConfig(issuer: string): Promise<OIDCConfig> {
  if (oidcConfig) return oidcConfig;

  const response = await fetch(`${issuer}/.well-known/openid-configuration`);
  oidcConfig = await response.json();

  return oidcConfig;
}

ベストプラクティス

  1. 必ず PKCE を使用する - 機密クライアントでもセキュリティが向上します
  2. state パラメーターを検証する - CSRF 攻撃を防ぎます
  3. httpOnly Cookie を使用する - リフレッシュトークンに使用し、localStorage には決して保存しないでください
  4. 有効期間の短いアクセストークンを使用する - 一般的には 15 分とし、必要に応じて更新します
  5. サーバー側でトークンを検証する - クライアント側の検証だけを信頼してはいけません
  6. nonce クレームを使用する - ID トークンを使ったリプレイ攻撃を防ぎます
  7. 適切なログアウトを実装する - トークンを失効させ、セッションを消去します
  8. audience と issuer を検証する - トークンが自分のアプリ向けであることを確認します
  9. リフレッシュトークンをローテーションする - 使用するたびに新しいリフレッシュトークンを発行します

よくある落とし穴

  • トークンを localStorage に保存する - XSS 攻撃に対して脆弱です
  • state を検証しない - CSRF の脆弱性を招きます
  • SPA で Implicit フローを使う - 非推奨です。Authorization Code + PKCE を使用してください
  • 有効期間の長いアクセストークンを使う - 漏えいした場合のリスクが高まります
  • JWT 署名を検証しない - 誰でも署名のないトークンを偽造できます
  • クライアントシークレットをハードコードする - 環境変数を使用してください
  • トークンの失効に対応しない - ユーザーが適切にログアウトできません
  • トークンの有効期限を処理しない - サイレント更新の失敗により UX が損なわれます

コメント