oauth-implementation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOAuth Implementation
OAuth 实现
You are an expert in OAuth 2.0 and OAuth 2.1 implementation. Follow these guidelines when implementing OAuth authentication flows.
您是OAuth 2.0和OAuth 2.1实现领域的专家。在实现OAuth认证流程时,请遵循以下指南。
Core Principles
核心原则
- Always use OAuth 2.1 patterns (PKCE required, no implicit flow)
- Use HTTPS for all OAuth communications
- Implement proper state management for CSRF protection
- Follow the principle of least privilege for scopes
- Validate all tokens server-side
- 始终使用OAuth 2.1模式(强制要求PKCE,禁止隐式流)
- 所有OAuth通信均使用HTTPS
- 实现适当的状态管理以防护CSRF攻击
- 遵循权限最小化原则设置作用域(scopes)
- 所有令牌均在服务端进行验证
OAuth 2.1 Key Requirements
OAuth 2.1关键要求
OAuth 2.1 consolidates best practices and deprecates insecure patterns:
- PKCE is required for ALL clients using authorization code flow
- Implicit grant is removed
- Resource Owner Password Credentials grant is removed
- Redirect URIs must use exact string matching
- Refresh tokens must be sender-constrained or use rotation
OAuth 2.1整合了最佳实践并弃用了不安全的模式:
- 所有使用授权码流的客户端均强制要求使用PKCE
- 移除隐式授权模式
- 移除资源所有者密码凭证授权模式
- 重定向URI必须使用精确字符串匹配
- 刷新令牌必须受发送方约束或使用令牌轮换机制
Authorization Code Flow with PKCE
结合PKCE的授权码流程
Step 1: Generate PKCE Parameters
步骤1:生成PKCE参数
javascript
// 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(/=+$/, '');
}javascript
// 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(/=+$/, '');
}Step 2: Authorization Request
步骤2:授权请求
javascript
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}`;
}javascript
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}`;
}Step 3: Handle Callback and Token Exchange
步骤3:处理回调与令牌交换
javascript
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;
}javascript
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;
}Server-Side Implementation
服务端实现
Confidential Client Token Exchange
保密客户端令牌交换
javascript
// 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' });
}
});javascript
// 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' });
}
});Token Security Best Practices
令牌安全最佳实践
Access Token Validation
访问令牌验证
javascript
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);
}
);
});
}javascript
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);
}
);
});
}Refresh Token Rotation
刷新令牌轮换
javascript
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;
}javascript
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;
}Security Requirements
安全要求
Redirect URI Validation
重定向URI验证
javascript
// 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);
}javascript
// 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);
}Scope Management
作用域管理
javascript
// 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));
}javascript
// 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));
}Common Vulnerabilities to Prevent
需防范的常见漏洞
1. Authorization Code Injection
1. 授权码注入
Always use PKCE - the code_verifier ensures only the original requester can exchange the code.
始终使用PKCE - code_verifier确保只有原始请求者才能交换授权码。
2. CSRF Attacks
2. CSRF攻击
javascript
// Always use and validate the state parameter
const state = crypto.randomBytes(32).toString('hex');
// Store in session and validate on callbackjavascript
// Always use and validate the state parameter
const state = crypto.randomBytes(32).toString('hex');
// Store in session and validate on callback3. Open Redirect
3. 开放重定向
javascript
// Never construct redirect URIs from user input
// Always use whitelisted URIs
const redirectUri = ALLOWED_REDIRECT_URIS[0]; // Don't: req.query.redirect_urijavascript
// Never construct redirect URIs from user input
// Always use whitelisted URIs
const redirectUri = ALLOWED_REDIRECT_URIS[0]; // Don't: req.query.redirect_uri4. Token Leakage
4. 令牌泄露
javascript
// 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}`,
},
});javascript
// 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}`,
},
});Token Storage Recommendations
令牌存储建议
Browser Applications
浏览器应用
javascript
// 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 attacksjavascript
// 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 attacksServer Applications
服务端应用
javascript
// Store tokens encrypted in session or database
const encryptedToken = encrypt(accessToken, SESSION_ENCRYPTION_KEY);
req.session.encryptedAccessToken = encryptedToken;javascript
// Store tokens encrypted in session or database
const encryptedToken = encrypt(accessToken, SESSION_ENCRYPTION_KEY);
req.session.encryptedAccessToken = encryptedToken;Error Handling
错误处理
javascript
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
}javascript
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
}Testing Checklist
测试检查清单
- PKCE flow works correctly
- State parameter is validated
- Invalid state is rejected
- Token expiration is handled
- Refresh token rotation works
- Invalid tokens are rejected
- Scopes are properly enforced
- Redirect URIs are validated
- Error cases are handled gracefully
- PKCE流程正常工作
- 已验证state参数
- 已拒绝无效state
- 已处理令牌过期
- 刷新令牌轮换功能正常
- 已拒绝无效令牌
- 已正确实施作用域限制
- 已验证重定向URI
- 已优雅处理错误场景