OAuth/OIDC 集成

本页提供授权码 + PKCE 完整实现(含 state 防 CSRF)、Token 存储策略安全对比、Refresh Token 轮换代码、ID Token 验证步骤,帮助 Agent 与后端以可审计方式对接第三方身份提供方。

优先授权码 + PKCE 于公共客户端;机密客户端保管 client_secret 于服务端。区分 access token(调用资源)与 id token(身份声明),校验 issuer、audience 与签名算法白名单。

SKILL 可引导列出 redirect URI、scope 与离线刷新策略;生成代码时强调 token 不落模型上下文、不写入仓库,刷新令牌仅服务端存储。

与 IdP 联调时覆盖错误路径:consent 拒绝、token 过期、revocation;文档中写明各环境的 well-known URL 与客户端 ID 占位符。

  • state/nonce 防 CSRF 与重放;严禁在 URL 中传递长期 secret。
  • 权限收敛:按最小 scope 申请,并在 UI 与日志中可解释。
  • 多租户时注意 tenant 专属的授权端点与元数据。

公共客户端:授权码 + PKCE 完整实现

  [ 用户代理:浏览器 / 原生 App ]
                    │
                    ▼
  [ 生成 code_verifier;code_challenge = BASE64URL(SHA256(verifier));method=S256 ]
                    │
                    ▼
  [ 重定向授权端点:response_type=code + code_challenge* + state + nonce(OIDC)]
                    │
                    ▼
  [ 用户在 IdP 登录并同意 scope ]
                    │
                    ▼
  [ 回调 redirect_uri:?code=…&state=… ]
                    │
                    ▼
  [ 校验 state;POST 令牌端点:code + code_verifier(通常无 client_secret)]
                    │
                    ▼
  [ access_token / id_token / refresh_token(若授权);校验 iss、aud、签名与 alg 白名单 ]
// 完整授权码 + PKCE 流程实现(浏览器端)
async function startAuthFlow() {
  // 生成 code_verifier(32字节随机)
  const verifier = btoa(String.fromCharCode(
    ...crypto.getRandomValues(new Uint8Array(32))
  )).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

  // 计算 code_challenge = BASE64URL(SHA256(verifier))
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

  // state 防 CSRF:随机值存入 sessionStorage,回调时校验
  const state = crypto.randomUUID();
  sessionStorage.setItem('pkce_verifier', verifier);
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'YOUR_CLIENT_ID',
    redirect_uri: 'https://app.example.com/callback',
    scope: 'openid profile email',
    state,                        // CSRF 防护
    code_challenge: challenge,
    code_challenge_method: 'S256',
    nonce: crypto.randomUUID(),   // OIDC 重放防护
  });
  window.location.href = `https://auth.example.com/authorize?${params}`;
}

// 回调处理:校验 state → 用 code + verifier 换 token
async function handleCallback(searchParams: URLSearchParams) {
  const returnedState = searchParams.get('state');
  const savedState = sessionStorage.getItem('oauth_state');
  if (returnedState !== savedState) throw new Error('State mismatch - CSRF attack');

  const code = searchParams.get('code')!;
  const verifier = sessionStorage.getItem('pkce_verifier')!;
  sessionStorage.removeItem('pkce_verifier');
  sessionStorage.removeItem('oauth_state');

  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    body: new URLSearchParams({ grant_type: 'authorization_code', code, code_verifier: verifier,
      client_id: 'YOUR_CLIENT_ID', redirect_uri: 'https://app.example.com/callback' }),
  });
  return response.json(); // { access_token, id_token, refresh_token }
}

code_verifier 仅在换票前保存在客户端侧安全存储;授权请求只带 code_challenge。禁止把 verifier 写进可分享链接或日志。

Token 存储策略与安全对比

// Token 存储三种策略安全对比

// ❌ 策略1: localStorage — XSS 可读,不推荐存 refresh_token
localStorage.setItem('access_token', token);   // 任何 JS 可读,XSS 直接泄露

// ⚠️ 策略2: Memory(变量)— 刷新页面丢失,但 XSS 受限
let accessToken: string | null = null;
function setAccessToken(t: string) { accessToken = t; }  // 页面刷新后丢失

// ✅ 策略3: HttpOnly Cookie — JS 不可读,推荐存 refresh_token
// 服务端设置(Node.js/Express):
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,    // JS 不可访问,防 XSS
  secure: true,      // 仅 HTTPS
  sameSite: 'strict', // 防 CSRF
  path: '/auth/refresh', // 限制 Cookie 发送路径
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
});
// access_token 存 Memory(短期,15min),refresh_token 存 HttpOnly Cookie

// Refresh Token 轮换实现:每次刷新返回新的 refresh_token
app.post('/auth/refresh', async (req, res) => {
  const oldRefreshToken = req.cookies.refresh_token;
  if (!oldRefreshToken) return res.status(401).json({ error: 'No refresh token' });

  try {
    // 从 IdP 换取新 token 对(rotation: 旧 refresh_token 同时失效)
    const { access_token, refresh_token: newRefresh } = await exchangeRefreshToken(oldRefreshToken);

    // 立即轮换:旧 token 已在 IdP 侧失效(检测重放)
    res.cookie('refresh_token', newRefresh, { httpOnly: true, secure: true, sameSite: 'strict' });
    res.json({ access_token }); // 仅返回短期 access_token
  } catch {
    res.clearCookie('refresh_token');
    res.status(401).json({ error: 'Refresh token revoked' }); // 可能是重放攻击
  }
});

ID Token 验证与 Refresh Token 轮换

// ID Token 完整验证步骤(OIDC 规范要求)
async function verifyIdToken(idToken: string, nonce: string) {
  // 1. 从发现文档获取 JWKS
  const discovery = await fetch('https://auth.example.com/.well-known/openid-configuration').then(r => r.json());
  const jwks = await fetch(discovery.jwks_uri).then(r => r.json());

  // 2. 验证签名(与 access token 相同流程)
  const payload = await verifySignature(idToken, jwks);

  // 3. 验证 iss:必须精确匹配 IdP URL
  if (payload.iss !== 'https://auth.example.com')
    throw new Error('Invalid issuer');

  // 4. 验证 aud:必须包含本应用的 client_id
  const audList = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
  if (!audList.includes('YOUR_CLIENT_ID'))
    throw new Error('Invalid audience');

  // 5. 验证 exp:token 未过期
  if (payload.exp < Math.floor(Date.now() / 1000))
    throw new Error('Token expired');

  // 6. 验证 nonce:防重放(必须与授权请求时一致)
  if (payload.nonce !== nonce)
    throw new Error('Nonce mismatch');

  return payload; // { sub, email, name, ... } — 身份声明
}
  • Access token:面向资源服务器;校验 audience、过期与 scope,不在前端长期明文存放。
  • 发现文档:/.well-known/openid-configuration 拉取 jwks_uri、end_session 等;生产禁用隐式信任自签名元数据。
  • 错误与吊销:处理 consent 拒绝、invalid_grant、时钟偏移;支持 revocation 时文档化客户端与用户的撤销入口。

PKCE 演示器(S256,仅本地)

使用浏览器 crypto.subtle 生成符合 RFC 7636 的随机 code_verifier(32 字节 → Base64URL)及 code_challenge(SHA-256 后再 Base64URL)。计算仅在本地完成,不会上传到服务器。

授权请求参数:code_challenge_method=S256code_challenge 填下框;换票时 POST code_verifier。勿将演示值用于生产账户,除非你完全控制 IdP 与 redirect 的测试环境。

---
name: oauth-oidc-integration
description: 起草或审查 OAuth2/OIDC 集成的安全实现
---
# 客户端类型与流程
1. 公共客户端(SPA/原生App):授权码 + PKCE(S256),无 client_secret
2. 机密客户端(后端服务):授权码 + PKCE + client_secret,服务端换票
3. 服务间调用:Client Credentials Flow,scope 最小化

# PKCE 实现要点
4. code_verifier:32字节随机,BASE64URL 编码
5. code_challenge:BASE64URL(SHA256(verifier)),method=S256
6. state:随机值存 sessionStorage,回调时精确校验,防 CSRF
7. nonce(OIDC):随机值绑定 ID Token,防重放

# Token 存储策略
8. access_token:Memory 存储,15min 有效期,不落 localStorage
9. refresh_token:HttpOnly Secure SameSite Cookie,限制 path
10. 每次刷新轮换 refresh_token,检测重放即吊销会话

# ID Token 验证(按顺序)
11. 验证签名:从 JWKS 端点获取公钥,算法白名单 RS256/ES256
12. 验证 iss:精确匹配 IdP URL
13. 验证 aud:必须包含本应用 client_id
14. 验证 exp:未过期(容忍30秒时钟偏移)
15. 验证 nonce:与授权请求时一致,防重放

返回技能库 更多技能入口