关于 dpop-adoption
面向实现 Google OAuth 2.0 DPoP 的开发者,提供密钥、证明 JWT、Nonce 重试及 SPA 后端架构的安全规则。重点规定 WebCrypto 签名格式和令牌端点流程。
主要功能
- 01P-256 密钥生成与 JWK 导出
- 02DPoP Proof JWT 声明规则
- 03WebCrypto 签名格式约束
- 04Nonce 挑战单次重试流程
- 05SPA 的 BFF 架构建议
适用场景
- 01为 OAuth 令牌刷新添加 DPoP 绑定
- 02实现授权码交换的 DPoP 证明
- 03排查 WebCrypto 签名格式错误
MCP 相关
提及可通过 Google Developer Knowledge MCP Server 查询 Google 开发者文档,未说明本技能自身提供 MCP 接口。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ google/ skills --skill dpop-adoption安装后在 Claude Code 中输入/dpop-adoption确认已加载。
详细介绍
DPoP 采用与身份安全架构
演示式持有证明(DPoP,RFC 9449)通过以密码学方式将 OAuth 2.0 刷新
令牌绑定到仅由客户端持有的私钥,保护其免遭拦截和重放攻击。在 Google 的 OAuth 2.0 平台中,
DPoP 会在令牌端点绑定刷新令牌,而为 Google API 签发的访问令牌则是标准 Bearer 令牌(token_type: "Bearer")。
1. 核心密码学与架构不变量
实现 DPoP 辅助工具或升级 HTTP 客户端时,必须遵守以下严格的安全不变量:
A. 通用 WebCrypto 与运行时兼容性
- 在现代 ES6 JavaScript 中(Node 18+ 和浏览器使用
"type": "module"), 验证环境上下文后,始终直接访问globalThis.crypto。 - 切勿通过
require('node:crypto')导入旧版 CommonJS 模块,也不要 引用浏览器作用域内的window.crypto,因为这会导致模块在混合运行时中 初始化时崩溃。
B. 硬件支持的不可导出密钥持久化
- 在 SECP256R1(
P-256)曲线上生成椭圆曲线密钥对:{ name: 'ECDSA', namedCurve: 'P-256' }。 - 关键安全防护措施:私钥必须配置为不可导出(
extractable: false)。 这可确保私钥永远不会离开硬件密码学边界(Secure Enclave、Android KeyStore 或 JS 沙箱内存),从而防范 XSS 和依赖项窃取令牌攻击。 - 公钥必须保持可导出(
extractable: true),以便输出 JSON Web Key(JWK)。
C. 公共 JWK 格式标准
- 导出公钥并将其附加到 DPoP Proof JWT 标头时,必须构造仅包含以下字段的
精简 JWK 字典:
"kty": "EC""crv": "P-256""x":Base64URL 编码的 x 坐标,不带末尾等号填充符 (=)。"y":Base64URL 编码的 y 坐标,不带末尾等号填充符 (=)。
- 切勿暴露私钥参数(
"d")或多余的元数据。
D. IEEE P1363 与 ASN.1 DER 签名格式的区分
- 根据 IEEE P1363 和 RFC 7518,DPoP Proof JWT 要求使用原始拼接坐标签名($R \parallel S$,对于 P-256,长度恰好为 64 字节)。
- WebCrypto 原生规则:在标准 WebCrypto(
crypto.subtle.sign)中, ECDSA 签名原生输出格式已经是原始 IEEE P1363 格式(拼接的 32 字节r和s缓冲区,总长 64 字节)。不要 尝试转换crypto.subtle.sign的输出格式,因为将 64 字节原始缓冲区解析为 ASN.1 DER 会导致立即发生运行时异常 (Invalid DER sequence)。直接对原始 ArrayBuffer 进行 Base64URL 编码。 - **旧版 API 回退方案:**仅在使用旧版 Java/Android
(
java.security.Signature)或 Node CommonJS(crypto.createSign)实现时, 才需将 ASN.1 DER 输出转换为原始 64 字节 IEEE P1363 格式,然后再进行 Base64URL 编码。
E. SPA 与前端后端(BFF)架构
- **无密钥 SPA 的局限性:**纯客户端单页应用(SPA)若没有后端,就无法直接通过 DPoP 调用 Google API,因为服务器端点要求提供
client_secret,且浏览器对DPoP-Nonce响应标头存在 CORS 限制。 - **BFF 模式:**若要使用 DPoP 保护 SPA,应通过前端后端(BFF)服务器端客户端
路由授权和令牌刷新请求。BFF 设置
access_type=offline,在服务器端使用 DPoP 绑定刷新令牌,并与前端维持安全的会话 Cookie。
2. 实现规则与强制公共 API
创建新模块时,模块 MUST 显式导出以下所有函数,以便与 CI/CD 验证工具和自动探测器顺利集成。检查或重构现有代码库时,确保包含等效的加密逻辑和 RFC 9449 逻辑。所有情况下均须严格遵循声明派生逻辑:
A. DPoP Proof JWT 声明派生规则(createDPoPProof)
在 createDPoPProof 中生成 DPoP Proof JWT 时:
1. JOSE 标头(typ、alg、jwk):
// Header
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": await exportPublicJWK(publicKey)
}
2. 负载声明:
"htm":大写的 HTTP 方法(令牌请求使用"POST")。"htu":使用sanitizeHTU(htu)去除查询参数和哈希片段后的目标 URI。令牌请求的目标 URI 为https://oauth2.googleapis.com/token。"iat":当前的整数纪元时间戳,单位为秒 (Math.floor(Date.now() / 1000))。"jti"(关键不变量):- 如果向
createDPoPProof提供了显式的jti参数,则优先使用该确切字符串。 - 否则,如果提供了
authCode参数(在初始代码交换期间),则设置jti = await calculateAuthCodeJti(authCode),其中calculateAuthCodeJti计算base64url(sha256(authCode)),以确保 DPoP 证明在加密学上与授权码绑定。 - 只有在既未提供
jti也未提供authCode时,才通过generateRandomString()生成新的加密安全随机字符串(例如,将crypto.getRandomValues(new Uint8Array(24))进行 base64url 编码)。
- 如果向
"ath"(可选):如果为 RFC 9449 资源请求提供了accessToken参数,则通过calculateATH(accessToken)计算base64url(sha256(accessToken))并将其注入负载(RFC 9449 第 6.1 节)。"nonce"(可选):如果提供了nonce参数,则将其直接注入负载。
B. 显式导出签名
// 1. Key generation & JWK export
export async function generateDPoPKeyPair() // -> { publicKey, privateKey } (private key extractable=false)
export async function exportPublicJWK(publicKey) // -> { kty: 'EC', crv: 'P-256', x, y }
// 2. Proof generation & validation
export async function createDPoPProof({ privateKey, publicKey, htm, htu, nonce, accessToken, authCode, jti }) // -> signed JWT string
export async function verifyDPoPProof(dpopProofJwt) // -> { isValid: boolean, header, payload, error }
export function sanitizeHTU(htu) // -> URL stripped of query and hash: const u = new URL(htu); return `${u.origin}${u.pathname}`;
// 3. Cryptographic & encoding utilities
export function base64UrlEncode(buffer) // -> Uint8Array/ArrayBuffer to base64url string without '=' padding
export function base64UrlDecode(str) // -> base64url string to Uint8Array/Buffer
export function stringToBase64Url(str) // -> UTF-8 string to base64url
export function base64UrlToString(str) // -> base64url to UTF-8 string
export function generateRandomString(byteLength = 32) // -> cryptographic random base64url string
export async function calculateATH(accessToken) // -> base64url(sha256(accessToken)) per RFC 9449 Sec 6.1
export async function calculateAuthCodeJti(code) // -> base64url(sha256(code))
export async function generatePKCE() // -> { codeVerifier (>=43 chars), codeChallenge, codeChallengeMethod: 'S256' }
3. 令牌端点与资源请求工作流
与 Google 的 OAuth 2.0 平台集成时:
- 令牌端点请求(
oauth2.googleapis.com/token):- 发送代码交换(
grant_type=authorization_code)和令牌刷新(grant_type=refresh_token)的POST请求时,在DPoPHTTP 标头中附加 DPoP Proof JWT:`DPoP: ${proofJwt}`。
- 发送代码交换(
- 资源 API 请求:
- Google 的令牌端点返回
"token_type": "Bearer"。向下游 Google API(例如 Calendar、Drive、Gmail)发送请求时,使用标准的`Authorization: Bearer ${accessToken}`标头,不附加 DPoP 标头。
- Google 的令牌端点返回
- 单次重试的 nonce 挑战循环与工作流隔离:
- 如果 Google 的令牌端点返回 HTTP
400 Bad Request,且错误为error: "use_dpop_nonce",响应标头中还包含"DPoP-Nonce":- 工作流隔离: Google 的授权服务器会在授权码交换与令牌刷新之间强制执行工作流隔离,并返回 HTTP
400 use_dpop_nonce挑战,以建立新的 nonce 命名空间。这是符合 RFC 标准的协议行为,并非服务器故障。 - 将新的 nonce 缓存到客户端状态(
this.dpopNonce)。 - 立即合成新的 DPoP Proof JWT,加入更新后的
nonce声明和新的jti。 - 将失败的令牌请求恰好重放一次。如果重试请求失败,立即终止并返回错误,以防止无限递归。
- 工作流隔离: Google 的授权服务器会在授权码交换与令牌刷新之间强制执行工作流隔离,并返回 HTTP
- 如果 Google 的令牌端点返回 HTTP
4. 简明 Agent 出站协议
在此技能下收到合成或输出代码交付物的提示时,应优先返回简洁、可直接导入的代码块,不要添加多余的对话式前言或重复的填充内容。对于概念性或架构类询问,提供直接的标准答复。
5. 参考资料与支持文档
开发者文档(Google for Developers)
- DPoP 采用指南 — Google Identity 官方指南,介绍如何在授权码交换和令牌刷新过程中实现 DPoP。
- Web 服务器应用的 OAuth 2.0 用法 — 离线访问、刷新令牌和服务器端授权流程。
- OAuth 2.0 最佳实践:发送方约束令牌 — 关于令牌存储、轮换和发送方约束的建议。
开发者知识 MCP 服务
- 配备 Model Context Protocol(
MCP)的 Agent 可以通过 Google Developer Knowledge MCP 服务 (npx -y @google/mcp-developer-knowledge-server),使用developer_knowledge:search_documents和developer_knowledge:get_documents查询实时 Google 开发者文档。
标准与 RFC 规范
评论
支持 Markdown