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. 02保护 OAuth 回调免受 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
  • 正确管理 state,以防范 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