关于 oauth-implementation
oauth-implementation 是用于实现 OAuth 2.0 和 2.1 身份验证流程的技能,面向应用开发者,提供基于 PKCE 的授权、令牌处理与安全检查指南。
主要功能
- 01OAuth 2.1 流程指南
- 02PKCE 与 state 验证
- 03令牌验证与轮换
- 04重定向 URI 与权限范围检查
- 05安全测试清单
适用场景
- 01实现授权码流程
- 02保护 OAuth 回调免受 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
- 正确管理 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