OAuth 2.0 访问令牌是访问 API 的密钥——一旦被盗,任何人都可以在任意位置使用它。DPoP(持有证明,Demonstrating Proof of Possession)定义于 RFC 9449,通过将令牌与请求方进行密码学绑定来解决这一问题。即使令牌被截获,没有客户端私钥也将毫无用处。
自 sgcWebSockets 2026.4.0 起,OAuth2 客户端和服务器组件均支持完整的 DPoP 功能。本文介绍 DPoP 的概念、工作原理以及在 Delphi 应用中的配置方法。
什么是 DPoP?
在标准 OAuth 2.0 中,Bearer 令牌如同现金——持有者即可使用。如果攻击者通过受损网络、泄露日志或恶意代理截获令牌,则可完全访问受保护资源。
DPoP 将令牌变成需要 PIN 的信用卡。令牌本身绑定到一个公钥,每次请求都必须附带由对应私钥签名的证明 JWT。服务器在接受请求前会验证证明与绑定密钥是否匹配。
|
Bearer 令牌(传统方式) 持有令牌即可使用。被盗令牌可被完全利用,无需证明身份。 |
DPoP 令牌(发送方约束) 令牌绑定到密钥对。每次请求都需附带新鲜的签名证明。没有私钥,被盗令牌毫无价值。 |
DPoP 工作原理
DPoP 流程在标准 OAuth2 流程中增加了一个轻量级密码学步骤:
1. 密钥生成 — 客户端生成非对称密钥对(ES256 或 RS256)。私钥留在客户端;公钥以 JWK 形式包含在 DPoP 证明中。
2. 令牌请求 — 请求访问令牌时,客户端在 HTTP 请求头中附加 DPoP,其中包含已签名的 JWT 证明。证明包含 HTTP 方法、URL、时间戳和唯一标识符。
3. 令牌绑定 — 授权服务器验证证明签名,提取 JWK 指纹(公钥的 SHA-256 哈希),并将其绑定到颁发的令牌上。响应中 token_type 为 DPoP 而非 Bearer。
4. 资源访问 — 每次 API 调用时,客户端同时发送令牌和新鲜的 DPoP 证明。证明包含 ath 声明(访问令牌的 SHA-256 哈希),将证明与该特定令牌绑定。
DPoP 证明的内部结构
DPoP 证明是一个由标头、载荷和签名三部分组成的紧凑 JWT。
// Header - identifies the key and algorithm
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "cWs37kZLJMej6fpd...",
"y": "e2bkcQGaBERgSZUb..."
}
}
// Payload - proves freshness and binds to the request
{
"htm": "POST",
"htu": "https://auth.example.com/oauth/token",
"iat": 1774950263,
"jti": "F1AFCD1F-95F7-401B-A2F5-195A31DB1802",
"ath": "fUHyO2r2Z3DZ53EsNr..."
}
在 sgcWebSockets 中配置 DPoP
第一步:生成 ES256 密钥对
使用 OpenSSL 生成椭圆曲线密钥对:
# Generate EC private key
openssl ecparam -name prime256v1 -genkey -noout -out dpop_private.pem
# Extract public key parameters
openssl ec -in dpop_private.pem -text -noout
将公钥的 X 和 Y 坐标转换为 Base64URL 以构建 JWK:
{"kty":"EC","crv":"P-256","x":"cWs37kZLJMej6fpdyKaI8Gz6CE...","y":"e2bkcQGaBERgSZUbAGR-iOOM..."}
第二步:配置 OAuth2 客户端
// Configure OAuth2 as usual
OAuth2.OAuth2Options.GrantType := auth2CodePKCE;
OAuth2.OAuth2Options.ClientId := 'your-client-id';
OAuth2.OAuth2Options.ClientSecret := 'your-client-secret';
OAuth2.AuthorizationServerOptions.AuthURL := 'https://auth.example.com/authorize';
OAuth2.AuthorizationServerOptions.TokenURL := 'https://auth.example.com/oauth/token';
// Enable DPoP
OAuth2.DPoPOptions.Enabled := True;
OAuth2.DPoPOptions.Algorithm := dpopES256;
OAuth2.DPoPOptions.PrivateKey.LoadFromFile('dpop_private.pem');
OAuth2.DPoPOptions.PublicKeyJWK := '{"kty":"EC","crv":"P-256","x":"...","y":"..."}';
// Start the OAuth2 flow - DPoP headers are added automatically
OAuth2.Start;
配置完成后,组件将自动:
- 为每次令牌请求生成新鲜的 DPoP 证明 JWT
- 透明处理
DPoP-Nonce服务器挑战并自动重试 - 保存 Nonce 以供后续请求使用
第三步:在 API 调用中使用 DPoP 证明
获取 DPoP 绑定的访问令牌后,为每次 API 请求生成证明:
var
vProof: String;
begin
// Generate a DPoP proof for the API call
vProof := OAuth2.GetDPoPProof(
'GET',
'https://api.example.com/userinfo',
OAuth2.AccessToken
);
// Include both headers in your HTTP request:
// Authorization: DPoP <access_token>
// DPoP: <proof_jwt>
HTTPClient.Request.CustomHeaders.AddValue('Authorization',
'DPoP ' + OAuth2.AccessToken);
HTTPClient.Request.CustomHeaders.AddValue('DPoP', vProof);
HTTPClient.Get('https://api.example.com/userinfo');
end;
支持的提供商
DPoP 已获主流 OAuth2 提供商支持:
| Auth0 | 在应用设置中启用 DPoP,需支持 Nonce(已自动处理)。 |
| Okta | 在授权服务器访问策略中配置 DPoP,自 2024 年起正式发布。 |
| Microsoft Entra ID | 支持机密客户端的 DPoP。 |
| Ping Identity | PingOne 和 PingFederate 均完整支持 DPoP。 |
服务器端 DPoP 验证
sgcWebSockets OAuth2 服务器组件同样支持 DPoP 验证。启用 OAuth2Options.DPoP 后,服务器将自动:
- 验证 DPoP 证明 JWT(签名、声明、新鲜度)
- 将令牌绑定到客户端的 JWK 指纹
- 对 DPoP 绑定令牌返回
token_type: DPoP - 拒绝证明无效或缺失的请求(返回 400
invalid_dpop_proof) - 验证资源请求中的
ath声明(访问令牌哈希)
// Enable DPoP on the OAuth2 server
OAuth2Server.OAuth2Options.DPoP := True;
// Optional: custom validation via event
OAuth2Server.OnOAuth2ValidateDPoP := procedure(Sender: TObject;
Connection: TsgcWSConnection;
const DPoPProof, AccessToken: String;
var IsValid: Boolean)
begin
// Add custom checks here (e.g., verify against a key registry)
IsValid := True;
end;
DPoP API 参考
| 属性 / 方法 | 说明 |
|---|---|
DPoPOptions.Enabled |
为所有令牌请求启用 DPoP。 |
DPoPOptions.Algorithm |
dpopES256(推荐)或 dpopRS256。 |
DPoPOptions.PrivateKey |
用于签名 DPoP 证明的 PEM 格式私钥。 |
DPoPOptions.PublicKeyJWK |
公钥的 JSON Web Key 表示。 |
GetDPoPProof() |
为指定的 HTTP 方法、URL 和访问令牌生成 DPoP 证明 JWT。 |
GetDPoPJWKThumbprint() |
返回公钥的 RFC 7638 SHA-256 指纹。 |
DPoPNonce |
服务器返回的当前 DPoP-Nonce 值(只读)。 |
提升令牌安全性
DPoP 是自 PKCE 以来 OAuth 2.0 令牌安全方面最重要的改进。它仅需极少的代码改动即可彻底消除整类令牌盗窃攻击——只需设置 DPoPOptions.Enabled := True,提供密钥,其余由 sgcWebSockets 组件自动处理。
DPoP 支持已在 sgcWebSockets 2026.4.0 中提供,适用于 Delphi(DXE6 至 D13)和 .NET(.NET Framework 2.0 至 .NET 9)。请前往 esegece.com 下载最新版本。
