auth-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Auth Security

认证安全

Core Principles

核心原则

  • OAuth 2.1 — Follow RFC 9700 (January 2025)
  • PKCE Required — All clients must use PKCE
  • Short-lived Tokens — Access tokens expire in 5-15 minutes
  • Token Rotation — Refresh tokens are single-use
  • HttpOnly Storage — Browser tokens in HttpOnly cookies
  • Explicit Algorithm — Never trust JWT header algorithm
  • No backwards compatibility — Delete deprecated auth flows

  • OAuth 2.1 — 遵循RFC 9700(2025年1月版)
  • 强制使用PKCE — 所有客户端必须使用PKCE
  • 短期令牌 — 访问令牌有效期为5-15分钟
  • 令牌轮转 — 刷新令牌为一次性使用
  • HttpOnly存储 — 浏览器令牌存储在HttpOnly Cookie中
  • 明确指定算法 — 绝不信任JWT头部中的算法
  • 无向后兼容性 — 删除已弃用的认证流程

OAuth 2.1 Key Changes

OAuth 2.1 主要变更

Deprecated Flows (DO NOT USE)

已弃用流程(禁止使用)

FlowStatusReplacement
Implicit GrantRemovedAuthorization Code + PKCE
Password GrantRemovedAuthorization Code + PKCE
Auth Code without PKCERemovedMust use PKCE
流程状态替代方案
隐式授权已移除授权码 + PKCE
密码授权已移除授权码 + PKCE
未使用PKCE的授权码已移除必须使用PKCE

Required: Authorization Code + PKCE

强制要求:授权码 + PKCE

typescript
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
  }),
});

typescript
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, // 证明请求由我方发起
  }),
});

JWT Best Practices

JWT 最佳实践

Algorithm Selection (2025)

算法选择(2025版)

PriorityAlgorithmNotes
1EdDSA (Ed25519)Most secure, quantum-resistant properties
2ES256 (ECDSA P-256)Widely supported, compact signatures
3PS256 (RSA-PSS)More secure than RS256
4RS256 (RSA PKCS#1)Best compatibility
typescript
// 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);
优先级算法说明
1EdDSA (Ed25519)安全性最高,具备抗量子特性
2ES256 (ECDSA P-256)支持广泛,签名紧凑
3PS256 (RSA-PSS)比RS256更安全
4RS256 (RSA PKCS#1)兼容性最佳
typescript
// 推荐使用: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);

Token Structure

令牌结构

typescript
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
}
typescript
interface AccessTokenPayload {
  // 标准声明
  iss: string;  // 签发者
  sub: string;  // 主体(用户ID)
  aud: string;  // 受众
  exp: number;  // 过期时间(Unix时间戳)
  iat: number;  // 签发时间
  jti: string;  // JWT ID(唯一标识符)

  // 自定义声明
  scope: string;      // 权限
  email?: string;     // 用户邮箱
  roles?: string[];   // 用户角色
}

Verification (Critical)

验证(关键步骤)

typescript
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');
  }
}

typescript
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');
  }
}

Token Storage

令牌存储

Web Applications

Web应用

typescript
// 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
  });
}
typescript
// 在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',  // 仅用于刷新端点
  });
}

Single Page Applications (SPA)

单页应用(SPA)

typescript
// 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
typescript
// 存储在内存中(禁止使用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 Comparison

存储方案对比

StorageXSS SafeCSRF SafePersistence
HttpOnly CookieYesNeeds SameSiteYes
MemoryYesYesNo (lost on reload)
localStorageNoYesYes
sessionStorageNoYesTab only

存储方式防XSS防CSRF持久性
HttpOnly Cookie需要SameSite配置
内存否(刷新页面后丢失)
localStorage
sessionStorage仅当前标签页有效

Refresh Token Rotation

刷新令牌轮转

Flow

流程

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 tokens
1. 客户端发送refresh_token
2. 服务端验证refresh_token
3. 服务端生成新的access_token + 新的refresh_token
4. 服务端作废旧的refresh_token
5. 服务端返回新令牌
6. 客户端存储新令牌

Implementation

实现代码

typescript
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,
  };
}

typescript
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,
  };
}

Attack Prevention

攻击防护

Algorithm Confusion

算法混淆攻击

typescript
// WRONG: Trusts header algorithm
jwt.verify(token, key); // Uses alg from header

// CORRECT: Explicit algorithm
jwt.verify(token, key, { algorithms: ['ES256'] });
typescript
// 错误做法:信任头部中的算法
jwt.verify(token, key); // 使用头部中的alg字段

// 正确做法:明确指定算法
jwt.verify(token, key, { algorithms: ['ES256'] });

CSRF Protection

CSRF防护

typescript
// 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
typescript
// 使用SameSite Cookie
res.cookie('session', token, {
  sameSite: 'strict', // 跨站链接可使用'lax'
});

// 或双提交Cookie模式
const csrfToken = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', csrfToken, { httpOnly: false });
// 客户端在请求头中携带csrf令牌

XSS Protection

XSS防护

typescript
// 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
typescript
// 内容安全策略
res.setHeader('Content-Security-Policy', [
  "default-src 'self'",
  "script-src 'self'",
  "style-src 'self' 'unsafe-inline'",
].join('; '));

// 使用HttpOnly Cookie存储令牌
// 绝不将令牌存储在localStorage中

Token Binding (DPoP)

令牌绑定(DPoP)

typescript
// 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,
  },
});

typescript
// 持有证明(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,
  },
});

Token Revocation

令牌作废

typescript
// 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;
}

typescript
// 作废用户所有令牌(例如:密码修改、全端登出)
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;
}

Checklist

检查清单

markdown
undefined
markdown
undefined

OAuth 2.1

OAuth 2.1

  • Using Authorization Code flow
  • PKCE enabled for all clients
  • No implicit or password grants
  • Redirect URI exact matching
  • 使用授权码流程
  • 所有客户端启用PKCE
  • 未使用隐式或密码授权
  • 重定向URI精确匹配

JWT

JWT

  • Using ES256 or EdDSA algorithm
  • Explicit algorithm verification
  • Short expiration (≤15 min)
  • Unique jti for each token
  • Issuer and audience validation
  • 使用ES256或EdDSA算法
  • 明确验证算法
  • 短期过期(≤15分钟)
  • 每个令牌使用唯一jti
  • 验证签发者和受众

Tokens

令牌管理

  • HttpOnly cookies for web apps
  • Refresh token rotation enabled
  • Reuse detection implemented
  • Token revocation mechanism
  • Web应用使用HttpOnly Cookie
  • 启用刷新令牌轮转
  • 实现复用检测
  • 具备令牌作废机制

Security

安全配置

  • HTTPS everywhere
  • SameSite cookies
  • CSP headers configured
  • Rate limiting on auth endpoints
  • Brute force protection

---
  • 全链路HTTPS
  • 配置SameSite Cookie
  • 配置CSP头部
  • 认证端点启用速率限制
  • 防暴力破解保护

---

See Also

相关参考

  • reference/oauth2.1.md — OAuth 2.1 deep dive
  • reference/jwt.md — JWT patterns
  • reference/attacks.md — Attack prevention
  • reference/oauth2.1.md — OAuth 2.1 深度解析
  • reference/jwt.md — JWT 实践模式
  • reference/attacks.md — 攻击防护指南