Loading...
Loading...
Compare original and translation side by side
| Flow | Status | Replacement |
|---|---|---|
| Implicit Grant | Removed | Authorization Code + PKCE |
| Password Grant | Removed | Authorization Code + PKCE |
| Auth Code without PKCE | Removed | Must use PKCE |
| 流程 | 状态 | 替代方案 |
|---|---|---|
| 隐式授权 | 已移除 | 授权码 + PKCE |
| 密码授权 | 已移除 | 授权码 + PKCE |
| 未使用PKCE的授权码 | 已移除 | 必须使用PKCE |
import crypto from 'crypto';
// 1. Generate code verifier (43-128 chars)
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
// 2. Generate code challenge
function generateCodeChallenge(verifier: string): string {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
// 3. Authorization request
const verifier = generateCodeVerifier();
const challenge = generateCodeChallenge(verifier);
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateState());
// 4. Token exchange (after redirect)
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: authorizationCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: verifier, // Prove we initiated the request
}),
});import crypto from 'crypto';
// 1. 生成code verifier(43-128个字符)
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
// 2. 生成code challenge
function generateCodeChallenge(verifier: string): string {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
// 3. 授权请求
const verifier = generateCodeVerifier();
const challenge = generateCodeChallenge(verifier);
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateState());
// 4. 令牌交换(重定向后)
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: authorizationCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: verifier, // 证明请求由我方发起
}),
});| Priority | Algorithm | Notes |
|---|---|---|
| 1 | EdDSA (Ed25519) | Most secure, quantum-resistant properties |
| 2 | ES256 (ECDSA P-256) | Widely supported, compact signatures |
| 3 | PS256 (RSA-PSS) | More secure than RS256 |
| 4 | RS256 (RSA PKCS#1) | Best compatibility |
// Recommended: ES256
import { SignJWT, jwtVerify } from 'jose';
const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'ES256');
const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'ES256');
// Sign
const token = await new SignJWT({ sub: userId, scope: 'read write' })
.setProtectedHeader({ alg: 'ES256', typ: 'JWT', kid: keyId })
.setIssuer('https://auth.example.com')
.setAudience('https://api.example.com')
.setExpirationTime('15m')
.setIssuedAt()
.setJti(crypto.randomUUID())
.sign(privateKey);| 优先级 | 算法 | 说明 |
|---|---|---|
| 1 | EdDSA (Ed25519) | 安全性最高,具备抗量子特性 |
| 2 | ES256 (ECDSA P-256) | 支持广泛,签名紧凑 |
| 3 | PS256 (RSA-PSS) | 比RS256更安全 |
| 4 | RS256 (RSA PKCS#1) | 兼容性最佳 |
// 推荐使用:ES256
import { SignJWT, jwtVerify } from 'jose';
const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'ES256');
const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'ES256');
// 签名
const token = await new SignJWT({ sub: userId, scope: 'read write' })
.setProtectedHeader({ alg: 'ES256', typ: 'JWT', kid: keyId })
.setIssuer('https://auth.example.com')
.setAudience('https://api.example.com')
.setExpirationTime('15m')
.setIssuedAt()
.setJti(crypto.randomUUID())
.sign(privateKey);interface AccessTokenPayload {
// Standard claims
iss: string; // Issuer
sub: string; // Subject (user ID)
aud: string; // Audience
exp: number; // Expiration (Unix timestamp)
iat: number; // Issued at
jti: string; // JWT ID (unique identifier)
// Custom claims
scope: string; // Permissions
email?: string; // User email
roles?: string[]; // User roles
}interface AccessTokenPayload {
// 标准声明
iss: string; // 签发者
sub: string; // 主体(用户ID)
aud: string; // 受众
exp: number; // 过期时间(Unix时间戳)
iat: number; // 签发时间
jti: string; // JWT ID(唯一标识符)
// 自定义声明
scope: string; // 权限
email?: string; // 用户邮箱
roles?: string[]; // 用户角色
}import { jwtVerify, errors } from 'jose';
async function verifyAccessToken(token: string): Promise<AccessTokenPayload> {
try {
const { payload } = await jwtVerify(token, publicKey, {
// CRITICAL: Explicitly specify allowed algorithms
algorithms: ['ES256'],
// Validate standard claims
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
// Clock tolerance for sync issues
clockTolerance: 30,
});
// Additional validation
if (!payload.scope?.includes('read')) {
throw new Error('Insufficient scope');
}
return payload as AccessTokenPayload;
} catch (err) {
if (err instanceof errors.JWTExpired) {
throw new AuthError('Token expired', 'TOKEN_EXPIRED');
}
if (err instanceof errors.JWTClaimValidationFailed) {
throw new AuthError('Invalid token claims', 'INVALID_CLAIMS');
}
throw new AuthError('Invalid token', 'INVALID_TOKEN');
}
}import { jwtVerify, errors } from 'jose';
async function verifyAccessToken(token: string): Promise<AccessTokenPayload> {
try {
const { payload } = await jwtVerify(token, publicKey, {
// 关键:明确指定允许的算法
algorithms: ['ES256'],
// 验证标准声明
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
// 时钟容错(解决时间同步问题)
clockTolerance: 30,
});
// 额外验证
if (!payload.scope?.includes('read')) {
throw new Error('权限不足');
}
return payload as AccessTokenPayload;
} catch (err) {
if (err instanceof errors.JWTExpired) {
throw new AuthError('令牌已过期', 'TOKEN_EXPIRED');
}
if (err instanceof errors.JWTClaimValidationFailed) {
throw new AuthError('令牌声明无效', 'INVALID_CLAIMS');
}
throw new AuthError('令牌无效', 'INVALID_TOKEN');
}
}// Set token in HttpOnly cookie (server-side)
function setAuthCookie(res: Response, token: string) {
res.cookie('access_token', token, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000, // 15 minutes
path: '/api', // Only sent to API routes
});
}
// Refresh token (longer-lived)
function setRefreshCookie(res: Response, token: string) {
res.cookie('refresh_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/api/auth/refresh', // Only for refresh endpoint
});
}// 在HttpOnly Cookie中设置令牌(服务端操作)
function setAuthCookie(res: Response, token: string) {
res.cookie('access_token', token, {
httpOnly: true, // 无法通过JavaScript访问
secure: true, // 仅在HTTPS下生效
sameSite: 'strict', // CSRF防护
maxAge: 15 * 60 * 1000, // 15分钟
path: '/api', // 仅发送到API路由
});
}
// 刷新令牌(有效期更长)
function setRefreshCookie(res: Response, token: string) {
res.cookie('refresh_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
path: '/api/auth/refresh', // 仅用于刷新端点
});
}// Store in memory (NOT localStorage/sessionStorage)
class TokenManager {
private accessToken: string | null = null;
setToken(token: string) {
this.accessToken = token;
}
getToken(): string | null {
return this.accessToken;
}
clearToken() {
this.accessToken = null;
}
}
// Use with Refresh Token Rotation
// Refresh token in HttpOnly cookie
// Access token in memory// 存储在内存中(禁止使用localStorage/sessionStorage)
class TokenManager {
private accessToken: string | null = null;
setToken(token: string) {
this.accessToken = token;
}
getToken(): string | null {
return this.accessToken;
}
clearToken() {
this.accessToken = null;
}
}
// 结合刷新令牌轮转使用
// 刷新令牌存储在HttpOnly Cookie中
// 访问令牌存储在内存中| Storage | XSS Safe | CSRF Safe | Persistence |
|---|---|---|---|
| HttpOnly Cookie | Yes | Needs SameSite | Yes |
| Memory | Yes | Yes | No (lost on reload) |
| localStorage | No | Yes | Yes |
| sessionStorage | No | Yes | Tab only |
| 存储方式 | 防XSS | 防CSRF | 持久性 |
|---|---|---|---|
| HttpOnly Cookie | 是 | 需要SameSite配置 | 是 |
| 内存 | 是 | 是 | 否(刷新页面后丢失) |
| localStorage | 否 | 是 | 是 |
| sessionStorage | 否 | 是 | 仅当前标签页有效 |
1. Client sends refresh_token
2. Server validates refresh_token
3. Server generates NEW access_token + NEW refresh_token
4. Server INVALIDATES old refresh_token
5. Server returns new tokens
6. Client stores new tokens1. 客户端发送refresh_token
2. 服务端验证refresh_token
3. 服务端生成新的access_token + 新的refresh_token
4. 服务端作废旧的refresh_token
5. 服务端返回新令牌
6. 客户端存储新令牌async function refreshTokens(refreshToken: string) {
// Find token in database
const stored = await db.refreshToken.findUnique({
where: { token: hashToken(refreshToken) },
include: { user: true },
});
if (!stored) {
throw new AuthError('Invalid refresh token', 'INVALID_TOKEN');
}
// Check if already used (reuse detection)
if (stored.usedAt) {
// Potential token theft - revoke ALL user tokens
await db.refreshToken.deleteMany({
where: { userId: stored.userId },
});
// Alert security team
await alertSecurityTeam({
event: 'REFRESH_TOKEN_REUSE',
userId: stored.userId,
tokenId: stored.id,
});
throw new AuthError('Token reuse detected', 'TOKEN_REUSE');
}
// Check expiration
if (stored.expiresAt < new Date()) {
throw new AuthError('Refresh token expired', 'TOKEN_EXPIRED');
}
// Mark as used (but keep for reuse detection)
await db.refreshToken.update({
where: { id: stored.id },
data: { usedAt: new Date() },
});
// Generate new tokens
const newAccessToken = await generateAccessToken(stored.user);
const newRefreshToken = await generateRefreshToken(stored.user);
// Store new refresh token
await db.refreshToken.create({
data: {
token: hashToken(newRefreshToken),
userId: stored.userId,
expiresAt: addDays(new Date(), 7),
previousTokenId: stored.id, // Chain for audit
},
});
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
};
}async function refreshTokens(refreshToken: string) {
// 在数据库中查找令牌
const stored = await db.refreshToken.findUnique({
where: { token: hashToken(refreshToken) },
include: { user: true },
});
if (!stored) {
throw new AuthError('刷新令牌无效', 'INVALID_TOKEN');
}
// 检查是否已被使用(复用检测)
if (stored.usedAt) {
// 可能存在令牌被盗风险 - 作废该用户的所有令牌
await db.refreshToken.deleteMany({
where: { userId: stored.userId },
});
// 通知安全团队
await alertSecurityTeam({
event: 'REFRESH_TOKEN_REUSE',
userId: stored.userId,
tokenId: stored.id,
});
throw new AuthError('检测到令牌复用', 'TOKEN_REUSE');
}
// 检查过期时间
if (stored.expiresAt < new Date()) {
throw new AuthError('刷新令牌已过期', 'TOKEN_EXPIRED');
}
// 标记为已使用(保留记录用于复用检测)
await db.refreshToken.update({
where: { id: stored.id },
data: { usedAt: new Date() },
});
// 生成新令牌
const newAccessToken = await generateAccessToken(stored.user);
const newRefreshToken = await generateRefreshToken(stored.user);
// 存储新的刷新令牌
await db.refreshToken.create({
data: {
token: hashToken(newRefreshToken),
userId: stored.userId,
expiresAt: addDays(new Date(), 7),
previousTokenId: stored.id, // 关联旧令牌用于审计
},
});
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
};
}// WRONG: Trusts header algorithm
jwt.verify(token, key); // Uses alg from header
// CORRECT: Explicit algorithm
jwt.verify(token, key, { algorithms: ['ES256'] });// 错误做法:信任头部中的算法
jwt.verify(token, key); // 使用头部中的alg字段
// 正确做法:明确指定算法
jwt.verify(token, key, { algorithms: ['ES256'] });// Use SameSite cookies
res.cookie('session', token, {
sameSite: 'strict', // or 'lax' for cross-site links
});
// Or double-submit cookie pattern
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', csrfToken, { httpOnly: false });
// Client sends csrf token in header// 使用SameSite Cookie
res.cookie('session', token, {
sameSite: 'strict', // 跨站链接可使用'lax'
});
// 或双提交Cookie模式
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', csrfToken, { httpOnly: false });
// 客户端在请求头中携带csrf令牌// Content Security Policy
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
].join('; '));
// Use HttpOnly cookies for tokens
// Never store tokens in localStorage// 内容安全策略
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
].join('; '));
// 使用HttpOnly Cookie存储令牌
// 绝不将令牌存储在localStorage中// Demonstration of Proof of Possession
// Bind token to client's key pair
const dpopProof = await new SignJWT({
htm: 'POST',
htu: 'https://api.example.com/resource',
ath: await hashAccessToken(accessToken), // Access token hash
})
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicKey })
.setJti(crypto.randomUUID())
.setIssuedAt()
.sign(privateKey);
// Send with request
fetch('https://api.example.com/resource', {
headers: {
Authorization: `DPoP ${accessToken}`,
DPoP: dpopProof,
},
});// 持有证明(Proof of Possession)示例
// 将令牌与客户端密钥对绑定
const dpopProof = await new SignJWT({
htm: 'POST',
htu: 'https://api.example.com/resource',
ath: await hashAccessToken(accessToken), // 访问令牌哈希
})
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicKey })
.setJti(crypto.randomUUID())
.setIssuedAt()
.sign(privateKey);
// 随请求发送
fetch('https://api.example.com/resource', {
headers: {
Authorization: `DPoP ${accessToken}`,
DPoP: dpopProof,
},
});// Revoke all user tokens (e.g., password change, logout all)
async function revokeAllUserTokens(userId: string) {
await db.refreshToken.deleteMany({
where: { userId },
});
// If using token blacklist for access tokens
await redis.sadd(`revoked:${userId}`, Date.now());
await redis.expire(`revoked:${userId}`, 15 * 60); // 15 min (access token lifetime)
}
// Check blacklist during verification
async function isTokenRevoked(userId: string, iat: number): Promise<boolean> {
const revokedAt = await redis.get(`revoked:${userId}`);
return revokedAt && parseInt(revokedAt) > iat * 1000;
}// 作废用户所有令牌(例如:密码修改、全端登出)
async function revokeAllUserTokens(userId: string) {
await db.refreshToken.deleteMany({
where: { userId },
});
// 如果使用令牌黑名单存储访问令牌
await redis.sadd(`revoked:${userId}`, Date.now());
await redis.expire(`revoked:${userId}`, 15 * 60); // 15分钟(访问令牌有效期)
}
// 验证时检查黑名单
async function isTokenRevoked(userId: string, iat: number): Promise<boolean> {
const revokedAt = await redis.get(`revoked:${userId}`);
return revokedAt && parseInt(revokedAt) > iat * 1000;
}undefinedundefined
---
---