安全与风控
在 WebCrypto/Node ES6…
当需要按 RFC 9449 为 Google OAuth 平台的授权码交换和令牌刷新实现 DPoP sender-constraining(含 400 use_dpop_nonce 重试处理)时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 WebCrypto/Node ES6特定
解决思路
- 在 Node 18+ 与浏览器的 ES6 模块(`"type": "module"`)中,确认运行环境后直接访问 `globalThis.crypto`;禁止用 `require('node:crypto')` 导入 legacy CommonJS,也禁止引用浏览器作用域的 `window.crypto`,否则混合运行时会在模块初始化阶段崩溃。
- 用 `{ name: 'ECDSA', namedCurve: 'P-256' }` 在 SECP256R1(P-256)曲线上生成椭圆曲线密钥对。
- 把私钥配置为不可导出(`extractable: false`),保证私钥不离开硬件密码学边界(Secure Enclave、Android KeyStore 或 JS 沙箱内存),以抵御 XSS 与依赖投毒导致的令牌窃取。
- 把公钥保持可导出(`extractable: true`),以便生成并输出 JWK。
- 导出公钥 JWK 时构造干净字典,只含 `"kty": "EC"`、`"crv": "P-256"`、`"x"`、`"y"`,其中 x/y 为 Base64URL 编码且不含尾部等号 padding(`=`);绝不暴露私钥参数 `"d"` 或多余元数据。
- 按 DPoP Proof JWT 要求使用 IEEE P1363 原始拼接坐标签名(R∥S,P-256 时恰好 64 字节);在标准 WebCrypto 的 `crypto.subtle.sign` 中 ECDSA 签名本来就是 raw IEEE P1363(32 字节 r + 32 字节 s),因此直接对 raw ArrayBuffer 做 base64url 编码,绝不要对 `crypto.subtle.sign` 的输出做 DER→Raw 转换,否则会把 64 字节 raw 缓冲当 ASN.1 DER 解析并立即抛 `Invalid DER sequence` 异常。
- 仅在使用 legacy Java/Android(`java.security.Signature`)或 Node CommonJS(`crypto.createSign`)时,才在 base64url 编码前把 ASN.1 DER 输出转换为 64 字节 IEEE P1363 raw 格式。
- 纯客户端 SPA(无后端)不能直接用 DPoP 访问 Google API,因为服务端端点要求 `client_secret` 且浏览器 CORS 限制 `DPoP-Nonce` 响应头;应改用 BFF(Backend-for-Frontend):由服务端客户端处理授权与令牌刷新,设置 `access_type=offline`,在服务端用 DPoP 绑定 refresh token,并与前端维护安全会话 cookie。
- 在 `createDPoPProof` 中构造 JOSE Header:`"typ": "dpop+jwt"`、`"alg": "ES256"`、`"jwk": await exportPublicJWK(publicKey)`。
- 在 payload 中设置 `"htm"` 为大写 HTTP 方法(令牌请求为 `"POST"`)。
- 在 payload 中设置 `"htu"` 为经 `sanitizeHTU(htu)` 去除 query 参数与 hash 片段后的目标 URI(令牌请求为 `https://oauth2.googleapis.com/token`),即 `const u = new URL(htu); return `${u.origin}${u.pathname}`;`。
- 在 payload 中设置 `"iat"` 为当前整数秒级 epoch 时间戳(`Math.floor(Date.now() / 1000)`)。
- 按优先级推导 `"jti"`:若显式传入 `jti` 参数,使用该字符串;否则若有 `authCode` 参数(初始授权码交换),用 `jti = await calculateAuthCodeJti(authCode)`,即 `base64url(sha256(authCode))`,把 DPoP proof 与授权码密码学绑定;仅当既无 `jti` 也无 `authCode` 时,才用 `generateRandomString()`(例如 `crypto.getRandomValues(new Uint8Array(24))` 后 base64url 编码)生成新的随机字符串。
- 若传入 `accessToken` 参数用于 RFC 9449 资源请求,通过 `calculateATH(accessToken)` 计算 `base64url(sha256(accessToken))` 并作为 `"ath"` claim 注入(RFC 9449 第 6.1 节)。
- 若传入 `nonce` 参数,直接把该 nonce 注入 payload 的 `"nonce"` claim。
- 导出 `generateDPoPKeyPair()`,返回 `{ publicKey, privateKey }`(私钥 `extractable=false`)。
- 导出 `exportPublicJWK(publicKey)`,返回 `{ kty: 'EC', crv: 'P-256', x, y }`。
- 导出 `createDPoPProof({ privateKey, publicKey, htm, htu, nonce, accessToken, authCode, jti })`,返回已签名 JWT 字符串。
- 导出 `verifyDPoPProof(dpopProofJwt)`,返回 `{ isValid: boolean, header, payload, error }`。
- 导出 `sanitizeHTU(htu)`,返回去除 query 与 hash 的 URL。
- 导出编码工具:`base64UrlEncode(buffer)`(Uint8Array/ArrayBuffer 转无 `=` padding 的 base64url 字符串)、`base64UrlDecode(str)`、`stringToBase64Url(str)`、`base64UrlToString(str)`。
- 导出 `generateRandomString(byteLength = 32)`,返回密码学随机的 base64url 字符串。
- 导出 `calculateATH(accessToken)` 计算 `base64url(sha256(accessToken))`(RFC 9449 第 6.1 节)与 `calculateAuthCodeJti(code)` 计算 `base64url(sha256(code))`。
- 导出 `generatePKCE()`,返回 `{ codeVerifier (>=43 chars), codeChallenge, codeChallengeMethod: 'S256' }`。
- 对 `oauth2.googleapis.com/token` 发起 `POST` 请求(`grant_type=authorization_code` 的授权码交换与 `grant_type=refresh_token` 的令牌刷新)时,在 HTTP 头附加 DPoP Proof JWT:`DPoP: ${proofJwt}`。
- 下游调用 Google API(Calendar、Drive、Gmail 等)时,因令牌端点返回 `"token_type": "Bearer"`,使用标准 `Authorization: Bearer ${accessToken}` 头,不带 DPoP 头。
- 处理 HTTP `400 Bad Request` 且 `error: "use_dpop_nonce"`(响应头含 `DPoP-Nonce`):先理解这是 Google 授权服务器在授权码交换与令牌刷新之间强制工作流隔离、建立全新 nonce 命名空间的标准 RFC 行为,不是服务端故障;然后把新 nonce 缓存到客户端状态(`this.dpopNonce`),立刻用更新后的 `nonce` claim 和新的 `jti` 合成新的 DPoP Proof JWT,并恰好重放失败请求一次;若重试仍失败则立即报错终止,防止无限递归。
- 输出代码交付物时,优先返回干净、可直接 import 的代码块,不要冗余的对话式前言与重复填充内容;对概念性或架构性问题则直接给出标准回答。
- 需要实时 Google 开发者文档时,可用 Google Developer Knowledge MCP Server(`npx -y @google/mcp-developer-knowledge-server`)通过 `developer_knowledge:search_documents` 与 `developer_knowledge:get_documents` 查询。
- 实现时对照官方文档与规范:Google DPoP Adoption Guide、Using OAuth 2.0 for Web Server Applications#offline、OAuth 2.0 Best Practices#sender-constrain-tokens,以及 RFC 9449、RFC 7519、RFC 7636。
适用边界 · 注意事项
- 不适用于未做 sender-constraining 的普通 OAuth 2.0 流程(refresh token 未与客户端密钥对绑定)。
- 不适用于 Google Cloud IAM / 服务账号(service account)认证场景。
- 纯客户端 SPA 且无后端、又必须直连 Google API 的场景不适用(受 `client_secret` 与 CORS 限制),需先引入 BFF。