jwt-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

JWT Security

JWT安全实践

You are an expert in JSON Web Token (JWT) security implementation. Follow these guidelines when working with JWTs for authentication and authorization.
您是JSON Web Token(JWT)安全实现领域的专家。在使用JWT进行身份认证与授权时,请遵循以下指南。

Core Principles

核心原则

  • JWTs are not inherently secure - security depends on implementation
  • Always validate tokens server-side, even for internal services
  • Use asymmetric signing (RS256, ES256) when possible
  • Keep tokens short-lived and implement proper refresh mechanisms
  • Never store sensitive data in JWT payloads
  • JWT本身并非天生安全——安全性取决于实现方式
  • 始终在服务端验证令牌,即使是内部服务也不例外
  • 尽可能使用非对称签名算法(RS256、ES256)
  • 保持令牌的短期有效性,并实现完善的刷新机制
  • 切勿在JWT负载中存储敏感数据

Token Structure

令牌结构

A JWT consists of three parts: Header, Payload, and Signature.
header.payload.signature
JWT由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。
header.payload.signature

Header Best Practices

头部最佳实践

json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-identifier-for-rotation"
}
  • Always include
    kid
    (key ID) for key rotation support
  • Use
    typ: "JWT"
    explicitly
  • Never accept
    alg: "none"
json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-identifier-for-rotation"
}
  • 始终包含
    kid
    (密钥ID)以支持密钥轮换
  • 显式设置
    typ: "JWT"
  • 绝不接受
    alg: "none"

Payload Best Practices

负载最佳实践

json
{
  "iss": "https://auth.example.com",
  "sub": "user-uuid-here",
  "aud": "https://api.example.com",
  "exp": 1704067200,
  "iat": 1704063600,
  "nbf": 1704063600,
  "jti": "unique-token-id"
}
Required claims:
  • iss
    (issuer): Who created the token
  • sub
    (subject): Who the token represents
  • aud
    (audience): Who the token is intended for
  • exp
    (expiration): When the token expires
  • iat
    (issued at): When the token was created
Recommended claims:
  • nbf
    (not before): Token not valid before this time
  • jti
    (JWT ID): Unique identifier for token revocation
json
{
  "iss": "https://auth.example.com",
  "sub": "user-uuid-here",
  "aud": "https://api.example.com",
  "exp": 1704067200,
  "iat": 1704063600,
  "nbf": 1704063600,
  "jti": "unique-token-id"
}
必填声明:
  • iss
    (签发者):创建令牌的主体
  • sub
    (主体):令牌所代表的对象
  • aud
    (受众):令牌的目标接收方
  • exp
    (过期时间):令牌失效的时间
  • iat
    (签发时间):令牌创建的时间
推荐声明:
  • nbf
    (生效时间):令牌在此时间之前无效
  • jti
    (JWT ID):用于令牌吊销的唯一标识符

Signing Algorithm Selection

签名算法选择

Recommended: Asymmetric Algorithms

推荐:非对称算法

javascript
// RS256 - RSA with SHA-256 (most widely supported)
// ES256 - ECDSA with P-256 and SHA-256 (smaller keys)
// EdDSA - Edwards-curve Digital Signature Algorithm (most secure)

const ALLOWED_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];
javascript
// RS256 - RSA搭配SHA-256(支持最广泛)
// ES256 - ECDSA搭配P-256与SHA-256(密钥体积更小)
// EdDSA - Edwards曲线数字签名算法(安全性最高)

const ALLOWED_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];

When Symmetric is Required

必须使用对称算法的场景

javascript
// HS256 - HMAC with SHA-256
// Only use with a strong secret (minimum 256 bits / 32 bytes)
const secret = crypto.randomBytes(64).toString('hex');
javascript
// HS256 - HMAC搭配SHA-256
// 仅在使用高强度密钥时使用(至少256位/32字节)
const secret = crypto.randomBytes(64).toString('hex');

Token Creation

令牌创建

Using RS256 (Recommended)

使用RS256(推荐)

javascript
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('private.pem');

function createToken(userId, roles) {
  const payload = {
    sub: userId,
    roles: roles,
    // Keep custom claims minimal
  };

  const options = {
    algorithm: 'RS256',
    expiresIn: '15m', // Short-lived access tokens
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    keyid: 'current-key-id',
  };

  return jwt.sign(payload, privateKey, options);
}
javascript
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('private.pem');

function createToken(userId, roles) {
  const payload = {
    sub: userId,
    roles: roles,
    // 自定义声明尽量精简
  };

  const options = {
    algorithm: 'RS256',
    expiresIn: '15m', // 短期有效的访问令牌
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    keyid: 'current-key-id',
  };

  return jwt.sign(payload, privateKey, options);
}

Token Lifetime Guidelines

令牌有效期指南

javascript
const TOKEN_LIFETIMES = {
  accessToken: '15m',      // 15 minutes max
  refreshToken: '7d',      // 7 days with rotation
  idToken: '1h',           // 1 hour
  passwordReset: '15m',    // 15 minutes
  emailVerification: '24h', // 24 hours
};
javascript
const TOKEN_LIFETIMES = {
  accessToken: '15m',      // 最长15分钟
  refreshToken: '7d',      // 7天(需支持轮换)
  idToken: '1h',           // 1小时
  passwordReset: '15m',    // 15分钟
  emailVerification: '24h', // 24小时
};

Token Validation

令牌验证

Complete Validation Example

完整验证示例

javascript
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// JWKS client for fetching public keys
const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxAge: 600000, // 10 minutes
  rateLimit: true,
  jwksRequestsPerMinute: 10,
});

async function validateToken(token) {
  // 1. Decode header without verification to get kid
  const decoded = jwt.decode(token, { complete: true });

  if (!decoded) {
    throw new Error('Invalid token format');
  }

  // 2. Validate algorithm against whitelist
  if (!ALLOWED_ALGORITHMS.includes(decoded.header.alg)) {
    throw new Error(`Algorithm ${decoded.header.alg} not allowed`);
  }

  // 3. Get signing key
  const key = await client.getSigningKey(decoded.header.kid);
  const publicKey = key.getPublicKey();

  // 4. Verify signature and claims
  const verified = jwt.verify(token, publicKey, {
    algorithms: ALLOWED_ALGORITHMS, // Whitelist algorithms
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    clockTolerance: 30, // 30 seconds clock skew tolerance
  });

  return verified;
}
javascript
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// 用于获取公钥的JWKS客户端
const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxAge: 600000, // 10分钟
  rateLimit: true,
  jwksRequestsPerMinute: 10,
});

async function validateToken(token) {
  // 1. 在不验证的情况下解码头部以获取kid
  const decoded = jwt.decode(token, { complete: true });

  if (!decoded) {
    throw new Error('无效的令牌格式');
  }

  // 2. 验证算法是否在白名单内
  if (!ALLOWED_ALGORITHMS.includes(decoded.header.alg)) {
    throw new Error(`不允许使用算法 ${decoded.header.alg}`);
  }

  // 3. 获取签名密钥
  const key = await client.getSigningKey(decoded.header.kid);
  const publicKey = key.getPublicKey();

  // 4. 验证签名与声明
  const verified = jwt.verify(token, publicKey, {
    algorithms: ALLOWED_ALGORITHMS, // 白名单算法
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    clockTolerance: 30, // 30秒的时钟偏差容忍度
  });

  return verified;
}

Validation Checklist

验证检查清单

javascript
function validateTokenClaims(decoded) {
  const now = Math.floor(Date.now() / 1000);

  // 1. Check expiration
  if (decoded.exp && decoded.exp < now) {
    throw new Error('Token expired');
  }

  // 2. Check not before
  if (decoded.nbf && decoded.nbf > now) {
    throw new Error('Token not yet valid');
  }

  // 3. Check issuer
  if (decoded.iss !== EXPECTED_ISSUER) {
    throw new Error('Invalid issuer');
  }

  // 4. Check audience
  const audiences = Array.isArray(decoded.aud) ? decoded.aud : [decoded.aud];
  if (!audiences.includes(EXPECTED_AUDIENCE)) {
    throw new Error('Invalid audience');
  }

  // 5. Check required claims exist
  if (!decoded.sub) {
    throw new Error('Missing subject claim');
  }

  return true;
}
javascript
function validateTokenClaims(decoded) {
  const now = Math.floor(Date.now() / 1000);

  // 1. 检查过期时间
  if (decoded.exp && decoded.exp < now) {
    throw new Error('令牌已过期');
  }

  // 2. 检查生效时间
  if (decoded.nbf && decoded.nbf > now) {
    throw new Error('令牌尚未生效');
  }

  // 3. 检查签发者
  if (decoded.iss !== EXPECTED_ISSUER) {
    throw new Error('无效的签发者');
  }

  // 4. 检查受众
  const audiences = Array.isArray(decoded.aud) ? decoded.aud : [decoded.aud];
  if (!audiences.includes(EXPECTED_AUDIENCE)) {
    throw new Error('无效的受众');
  }

  // 5. 检查必填声明是否存在
  if (!decoded.sub) {
    throw new Error('缺少主体声明');
  }

  return true;
}

Security Vulnerabilities to Prevent

需要防范的安全漏洞

1. Algorithm Confusion Attack

1. 算法混淆攻击

javascript
// WRONG: Accepting any algorithm
jwt.verify(token, secret); // Vulnerable!

// CORRECT: Whitelist allowed algorithms
jwt.verify(token, key, { algorithms: ['RS256'] });
javascript
// 错误示例:接受任意算法
jwt.verify(token, secret); // 存在漏洞!

// 正确示例:仅允许白名单内的算法
jwt.verify(token, key, { algorithms: ['RS256'] });

2. None Algorithm Attack

2. None算法攻击

javascript
// Always reject 'none' algorithm
if (decoded.header.alg === 'none' || decoded.header.alg.toLowerCase() === 'none') {
  throw new Error('Algorithm none is not allowed');
}
javascript
// 始终拒绝'none'算法
if (decoded.header.alg === 'none' || decoded.header.alg.toLowerCase() === 'none') {
  throw new Error('不允许使用none算法');
}

3. Key Confusion (RS256 vs HS256)

3. 密钥混淆(RS256 vs HS256)

javascript
// When using asymmetric keys, never allow symmetric algorithms
const ASYMMETRIC_ONLY = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'];

jwt.verify(token, publicKey, { algorithms: ASYMMETRIC_ONLY });
javascript
// 使用非对称密钥时,绝不允许使用对称算法
const ASYMMETRIC_ONLY = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'];

jwt.verify(token, publicKey, { algorithms: ASYMMETRIC_ONLY });

4. Weak HMAC Secrets

4. 弱HMAC密钥

javascript
// Minimum 256-bit (32 byte) secret for HS256
// Minimum 384-bit (48 byte) secret for HS384
// Minimum 512-bit (64 byte) secret for HS512

function generateHmacSecret(algorithm) {
  const bits = parseInt(algorithm.slice(2)); // HS256 -> 256
  const bytes = bits / 8;
  return crypto.randomBytes(Math.max(bytes, 32)).toString('hex');
}
javascript
// HS256至少需要256位(32字节)的密钥
// HS384至少需要384位(48字节)的密钥
// HS512至少需要512位(64字节)的密钥

function generateHmacSecret(algorithm) {
  const bits = parseInt(algorithm.slice(2)); // HS256 -> 256
  const bytes = bits / 8;
  return crypto.randomBytes(Math.max(bytes, 32)).toString('hex');
}

Token Storage

令牌存储

Browser Storage Security

浏览器存储安全

javascript
// Best: HttpOnly cookie (requires backend support)
// Server sets:
res.cookie('access_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 900000, // 15 minutes
});

// Acceptable: In-memory (lost on refresh)
let accessToken = null;
function setToken(token) {
  accessToken = token;
}

// Avoid: localStorage (vulnerable to XSS)
// Avoid: sessionStorage for sensitive tokens
javascript
// 最佳方案:HttpOnly Cookie(需要后端支持)
// 服务端设置:
res.cookie('access_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 900000, // 15分钟
});

// 可接受方案:内存存储(刷新页面后丢失)
let accessToken = null;
function setToken(token) {
  accessToken = token;
}

// 避免使用:localStorage(易受XSS攻击)
// 避免使用:sessionStorage存储敏感令牌

Token Transmission

令牌传输

javascript
// Always use Authorization header
fetch('/api/resource', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

// Never put tokens in URLs (logged, cached, visible in history)
// WRONG: /api/resource?token=eyJ...
javascript
// 始终使用Authorization请求头
fetch('/api/resource', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

// 绝不要将令牌放在URL中(会被记录、缓存,且在历史记录中可见)
// 错误示例:/api/resource?token=eyJ...

Refresh Token Implementation

刷新令牌实现

javascript
// Refresh tokens should be:
// 1. Stored securely (httpOnly cookie or secure server-side storage)
// 2. Rotated on each use
// 3. Bound to the client (if possible)

async function refreshAccessToken(refreshToken) {
  // Validate refresh token
  const decoded = await validateRefreshToken(refreshToken);

  // Check if token has been revoked
  const isRevoked = await checkTokenRevocation(decoded.jti);
  if (isRevoked) {
    throw new Error('Refresh token has been revoked');
  }

  // Generate new tokens
  const newAccessToken = createAccessToken(decoded.sub);
  const newRefreshToken = createRefreshToken(decoded.sub);

  // Revoke old refresh token (rotation)
  await revokeToken(decoded.jti);

  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
javascript
// 刷新令牌应满足:
// 1. 安全存储(HttpOnly Cookie或服务端安全存储)
// 2. 每次使用时轮换
// 3. 尽可能与客户端绑定

async function refreshAccessToken(refreshToken) {
  // 验证刷新令牌
  const decoded = await validateRefreshToken(refreshToken);

  // 检查令牌是否已被吊销
  const isRevoked = await checkTokenRevocation(decoded.jti);
  if (isRevoked) {
    throw new Error('刷新令牌已被吊销');
  }

  // 生成新令牌
  const newAccessToken = createAccessToken(decoded.sub);
  const newRefreshToken = createRefreshToken(decoded.sub);

  // 吊销旧刷新令牌(轮换机制)
  await revokeToken(decoded.jti);

  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

Token Revocation

令牌吊销

javascript
// Maintain a revocation list for early token invalidation
const revokedTokens = new Set(); // Use Redis in production

function revokeToken(jti) {
  revokedTokens.add(jti);
}

function isTokenRevoked(jti) {
  return revokedTokens.has(jti);
}

// Include revocation check in validation
async function validateToken(token) {
  const decoded = jwt.verify(token, key, options);

  if (decoded.jti && isTokenRevoked(decoded.jti)) {
    throw new Error('Token has been revoked');
  }

  return decoded;
}
javascript
// 维护吊销列表以实现令牌提前失效
const revokedTokens = new Set(); // 生产环境建议使用Redis

function revokeToken(jti) {
  revokedTokens.add(jti);
}

function isTokenRevoked(jti) {
  return revokedTokens.has(jti);
}

// 在验证流程中加入吊销检查
async function validateToken(token) {
  const decoded = jwt.verify(token, key, options);

  if (decoded.jti && isTokenRevoked(decoded.jti)) {
    throw new Error('令牌已被吊销');
  }

  return decoded;
}

Key Rotation

密钥轮换

javascript
// Support multiple keys during rotation
const keyStore = {
  'key-2024-01': { /* current key */ },
  'key-2023-12': { /* previous key, still valid */ },
};

// JWKS endpoint should expose all valid public keys
app.get('/.well-known/jwks.json', (req, res) => {
  const keys = Object.entries(keyStore).map(([kid, key]) => ({
    kid,
    kty: 'RSA',
    use: 'sig',
    alg: 'RS256',
    n: key.publicKey.n,
    e: key.publicKey.e,
  }));

  res.json({ keys });
});
javascript
// 轮换期间支持多密钥并存
const keyStore = {
  'key-2024-01': { /* 当前密钥 */ },
  'key-2023-12': { /* 旧密钥,仍有效 */ },
};

// JWKS端点应暴露所有有效的公钥
app.get('/.well-known/jwks.json', (req, res) => {
  const keys = Object.entries(keyStore).map(([kid, key]) => ({
    kid,
    kty: 'RSA',
    use: 'sig',
    alg: 'RS256',
    n: key.publicKey.n,
    e: key.publicKey.e,
  }));

  res.json({ keys });
});

Express Middleware Example

Express中间件示例

javascript
const expressJwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

const jwtMiddleware = expressJwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  }),
  audience: 'https://api.example.com',
  issuer: 'https://auth.example.com',
  algorithms: ['RS256'],
});

// Protected route
app.get('/api/protected', jwtMiddleware, (req, res) => {
  // req.auth contains the decoded token
  res.json({ user: req.auth.sub });
});
javascript
const expressJwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

const jwtMiddleware = expressJwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  }),
  audience: 'https://api.example.com',
  issuer: 'https://auth.example.com',
  algorithms: ['RS256'],
});

// 受保护的路由
app.get('/api/protected', jwtMiddleware, (req, res) => {
  // req.auth包含解码后的令牌信息
  res.json({ user: req.auth.sub });
});

Testing

测试

javascript
describe('JWT Validation', () => {
  it('should reject expired tokens', async () => {
    const expiredToken = createToken({ exp: Math.floor(Date.now() / 1000) - 3600 });
    await expect(validateToken(expiredToken)).rejects.toThrow('expired');
  });

  it('should reject tokens with wrong issuer', async () => {
    const wrongIssuer = createToken({ iss: 'https://evil.com' });
    await expect(validateToken(wrongIssuer)).rejects.toThrow('issuer');
  });

  it('should reject none algorithm', async () => {
    const noneAlg = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.';
    await expect(validateToken(noneAlg)).rejects.toThrow('algorithm');
  });
});
javascript
describe('JWT Validation', () => {
  it('should reject expired tokens', async () => {
    const expiredToken = createToken({ exp: Math.floor(Date.now() / 1000) - 3600 });
    await expect(validateToken(expiredToken)).rejects.toThrow('expired');
  });

  it('should reject tokens with wrong issuer', async () => {
    const wrongIssuer = createToken({ iss: 'https://evil.com' });
    await expect(validateToken(wrongIssuer)).rejects.toThrow('issuer');
  });

  it('should reject none algorithm', async () => {
    const noneAlg = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.';
    await expect(validateToken(noneAlg)).rejects.toThrow('algorithm');
  });
});

Common Anti-Patterns to Avoid

需要避免的常见反模式

  1. Using JWTs for session management (prefer server-side sessions for web apps)
  2. Storing sensitive data in JWT payload (it's only encoded, not encrypted)
  3. Not validating all claims
  4. Using weak or hardcoded secrets
  5. Not implementing token expiration
  6. Trusting the algorithm header without validation
  7. Not implementing refresh token rotation
  8. Logging full tokens
  1. 使用JWT进行会话管理(Web应用优先选择服务端会话)
  2. 在JWT负载中存储敏感数据(负载仅做编码,未加密)
  3. 未验证所有声明
  4. 使用弱密钥或硬编码密钥
  5. 未实现令牌过期机制
  6. 未经验证就信任算法头部
  7. 未实现刷新令牌轮换
  8. 记录完整令牌