此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.5.0spring-doc.cadn.net.cn

OAuth 2.0 DPoP 绑定访问Tokens

RFC 9449 OAuth 2.0 演示所有权证明 (DPoP) 是一种应用程序级机制,用于对访问Tokens进行发送者约束。spring-doc.cadn.net.cn

DPoP 的主要目标是防止未经授权或非法的客户端使用泄露或被盗的访问Tokens,方法是在授权服务器颁发访问Tokens时将访问Tokens绑定到公钥,并要求客户端在资源服务器上使用访问Tokens时证明拥有相应的私钥。spring-doc.cadn.net.cn

通过 DPoP 受发送方约束的访问Tokens与典型的不记名Tokens形成鲜明对比,后者可由拥有访问Tokens的任何客户端使用。spring-doc.cadn.net.cn

DPoP 引入了 DPoP 证明的概念,它是由客户端创建并作为 HTTP 请求中的标头发送的 JWT。 客户端使用 DPoP 证明来证明拥有与某个公钥对应的私钥。spring-doc.cadn.net.cn

当客户端发起访问Tokens请求时,它会在 HTTP 标头中将 DPoP 证明附加到请求。 授权服务器将访问Tokens绑定(发送者约束)到 DPoP 证明中关联的公钥。spring-doc.cadn.net.cn

当客户端发起受保护资源请求时,它会再次将 DPoP 证明附加到 HTTP 标头中的请求。spring-doc.cadn.net.cn

资源服务器直接在访问Tokens (JWT) 中或通过Tokens自省终端节点获取有关绑定到访问Tokens的公钥的信息。 然后,资源服务器验证绑定到访问Tokens的公钥是否与 DPoP 证明中的公钥匹配。 它还验证 DPoP 证明中的访问Tokens哈希是否与请求中的访问Tokens匹配。spring-doc.cadn.net.cn

DPoP 访问Tokens请求

要请求使用 DPoP 绑定到公钥的访问Tokens,客户端必须在DPoP标头。 这适用于所有访问Tokens请求,无论授权授权类型如何(例如authorization_code,refresh_token,client_credentials等)。spring-doc.cadn.net.cn

以下 HTTP 请求显示了authorization_code访问Tokens请求,并在DPoP页眉:spring-doc.cadn.net.cn

POST /oauth2/token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJraWQiOiJyc2EtandrLWtpZCIsInR5cCI6ImRwb3Arand0IiwiYWxnIjoiUlMyNTYiLCJqd2siOnsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJraWQiOiJyc2EtandrLWtpZCIsIm4iOiIzRmxxSnI1VFJza0lRSWdkRTNEZDdEOWxib1dkY1RVVDhhLWZKUjdNQXZRbTdYWE5vWWttM3Y3TVFMMU5ZdER2TDJsOENBbmMwV2RTVElOVTZJUnZjNUtxbzJRNGNzTlg5U0hPbUVmem9ST2pRcWFoRWN2ZTFqQlhsdW9DWGRZdVlweDRfMXRmUmdHNmlpNFVoeGg2aUk4cU5NSlFYLWZMZnFoYmZZZnhCUVZSUHl3QmtBYklQNHgxRUFzYkM2RlNObWtoQ3hpTU5xRWd4YUlwWThDMmtKZEpfWklWLVdXNG5vRGR6cEtxSGN3bUI4RnNydW1sVllfRE5WdlVTRElpcGlxOVBiUDRIOTlUWE4xbzc0Nm9SYU5hMDdycTFob0NnTVNTeS04NVNhZ0NveGxteUUtRC1vZjlTc01ZOE9sOXQwcmR6cG9iQnVoeUpfbzVkZnZqS3cifX0.eyJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20vb2F1dGgyL3Rva2VuIiwiaWF0IjoxNzQ2ODA2MzA1LCJqdGkiOiI0YjIzNDBkMi1hOTFmLTQwYTUtYmFhOS1kZDRlNWRlYWM4NjcifQ.wq8gJ_G6vpiEinfaY3WhereqCCLoeJOG8tnWBBAzRWx9F1KU5yAAWq-ZVCk_k07-h6DIqz2wgv6y9dVbNpRYwNwDUeik9qLRsC60M8YW7EFVyI3n_NpujLwzZeub_nDYMVnyn4ii0NaZrYHtoGXOlswQfS_-ET-jpC0XWm5nBZsCdUEXjOYtwaACC6Js-pyNwKmSLp5SKIk11jZUR5xIIopaQy521y9qJHhGRwzj8DQGsP7wMZ98UFL0E--1c-hh4rTy8PMeWCqRHdwjj_ry_eTe0DJFcxxYQdeL7-0_0CIO4Ayx5WHEpcUOIzBRoN32RsNpDZc-5slDNj9ku004DA

grant_type=authorization_code\
&client_id=s6BhdRkqt\
&code=SplxlOBeZQQYbYS6WxSbIA\
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb\
&code_verifier=bEaL42izcC-o-xBk0K2vuJ6U-y1p9r_wW2dFWIWgjz-

下面显示了 DPoP Proof JWT 标头和声明的表示形式:spring-doc.cadn.net.cn

{
  "typ": "dpop+jwt",
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "e": "AQAB",
    "n": "3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw"
  }
}
{
  "htm": "POST",
  "htu": "https://server.example.com/oauth2/token",
  "iat": 1746806305,
  "jti": "4b2340d2-a91f-40a5-baa9-dd4e5deac867"
}

以下代码显示了如何生成 DPoP 证明 JWT 的示例:spring-doc.cadn.net.cn

RSAKey rsaKey = ...
JWKSource<SecurityContext> jwkSource = (jwkSelector, securityContext) -> jwkSelector
		.select(new JWKSet(rsaKey));
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);

JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
		.type("dpop+jwt")
		.jwk(rsaKey.toPublicJWK().toJSONObject())
		.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
		.issuedAt(Instant.now())
		.claim("htm", "POST")
		.claim("htu", "https://server.example.com/oauth2/token")
		.id(UUID.randomUUID().toString())
		.build();

Jwt dPoPProof = jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));

授权服务器成功验证 DPoP 证明后,DPoP 证明中的公钥将被绑定(发件人约束)到颁发的访问Tokens。spring-doc.cadn.net.cn

以下访问Tokens响应显示token_typeparameter 指定为DPoP向客户端发出信号,表明访问Tokens已绑定到其 DPoP 证明公钥:spring-doc.cadn.net.cn

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
 "access_token": "Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxU",
 "token_type": "DPoP",
 "expires_in": 2677
}

公钥确认

资源服务器必须能够识别访问Tokens是否受 DPoP 绑定,并验证是否绑定到 DPoP 证明的公钥。 绑定是通过以资源服务器可以访问的方式将公钥与访问Tokens相关联来实现的,例如将公钥哈希直接嵌入访问Tokens (JWT) 或通过Tokens自省。spring-doc.cadn.net.cn

当访问Tokens表示为 JWT 时,公钥哈希包含在jkt确认方法 (cnf) 声明。spring-doc.cadn.net.cn

以下示例显示了包含cnf声明中带有jktclaim,这是 DPoP 证明公钥的 JWK SHA-256 指纹:spring-doc.cadn.net.cn

{
  "sub":"[email protected]",
  "iss":"https://server.example.com",
  "nbf":1562262611,
  "exp":1562266216,
  "cnf":
  {
    "jkt":"CQMknzRoZ5YUi7vS58jck1q8TmZT8wiIiXrCN1Ny4VU"
  }
}

DPoP 保护资源请求

对受 DPoP 保护的资源的请求必须同时包括 DPoP 证明和 DPoP 绑定的访问Tokens。 DPoP 证明必须包括athclaim 替换为访问Tokens的有效哈希值。 资源服务器将计算收到的访问Tokens的哈希值,并验证它是否与ath声明。spring-doc.cadn.net.cn

DPoP 绑定的访问Tokens是使用Authorization请求标头,其身份验证方案为DPoP.spring-doc.cadn.net.cn

以下 HTTP 请求显示了一个受保护的资源请求,该请求在Authorizationheader 和 DPoP 校样在DPoP页眉:spring-doc.cadn.net.cn

GET /resource HTTP/1.1
Host: resource.example.com
Authorization: DPoP Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxU
DPoP: eyJraWQiOiJyc2EtandrLWtpZCIsInR5cCI6ImRwb3Arand0IiwiYWxnIjoiUlMyNTYiLCJqd2siOnsia3R5IjoiUlNBIiwiZSI6IkFRQUIiLCJraWQiOiJyc2EtandrLWtpZCIsIm4iOiIzRmxxSnI1VFJza0lRSWdkRTNEZDdEOWxib1dkY1RVVDhhLWZKUjdNQXZRbTdYWE5vWWttM3Y3TVFMMU5ZdER2TDJsOENBbmMwV2RTVElOVTZJUnZjNUtxbzJRNGNzTlg5U0hPbUVmem9ST2pRcWFoRWN2ZTFqQlhsdW9DWGRZdVlweDRfMXRmUmdHNmlpNFVoeGg2aUk4cU5NSlFYLWZMZnFoYmZZZnhCUVZSUHl3QmtBYklQNHgxRUFzYkM2RlNObWtoQ3hpTU5xRWd4YUlwWThDMmtKZEpfWklWLVdXNG5vRGR6cEtxSGN3bUI4RnNydW1sVllfRE5WdlVTRElpcGlxOVBiUDRIOTlUWE4xbzc0Nm9SYU5hMDdycTFob0NnTVNTeS04NVNhZ0NveGxteUUtRC1vZjlTc01ZOE9sOXQwcmR6cG9iQnVoeUpfbzVkZnZqS3cifX0.eyJodG0iOiJHRVQiLCJodHUiOiJodHRwczovL3Jlc291cmNlLmV4YW1wbGUuY29tL3Jlc291cmNlIiwiYXRoIjoiZlVIeU8ycjJaM0RaNTNFc05yV0JiMHhXWG9hTnk1OUlpS0NBcWtzbVFFbyIsImlhdCI6MTc0NjgwNzEzOCwianRpIjoiM2MyZWU5YmItMDNhYy00MGNmLWI4MTItMDBiZmJhMzQxY2VlIn0.oS6NwjURR6wZemh1ZBNiBjycGeXwnkguLtgiKdCjQSEhFQpEJm04bBa0tdfZgWT17Z2mBgddnNQSkROzUGfssg8rBBldZXOAiduF-whtEGZA-pXXWJilXrwH3Glb6hIOMZOVmIH8fmYCDmqn-sE_DmDIsv57Il2-jdZbgeDcrxADO-6E5gsuNf1jvy7qqHq7INrKX6jRuydti_Re35lecvaAWfTyD7s7tQ_-3x_xLxxPwf_eA6z8OWbc58O2PYoUeO2JKLiOIg6UVZOZzxLEWV42WIKjha_kkoykvsf98W2y8pWOEr65u0VPsn5esw2X3I1eFL_A-XkxstZHRaGXJg

下面显示了 DPoP Proof JWT 标头和声明的表示形式,其中包含ath索赔:spring-doc.cadn.net.cn

{
  "typ": "dpop+jwt",
  "alg": "RS256",
  "jwk": {
    "kty": "RSA",
    "e": "AQAB",
    "n": "3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw"
  }
}
{
  "htm": "GET",
  "htu": "https://resource.example.com/resource",
  "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo",
  "iat": 1746807138,
  "jti": "3c2ee9bb-03ac-40cf-b812-00bfba341cee"
}

以下代码显示了如何生成 DPoP 证明 JWT 的示例:spring-doc.cadn.net.cn

RSAKey rsaKey = ...
JWKSource<SecurityContext> jwkSource = (jwkSelector, securityContext) -> jwkSelector
		.select(new JWKSet(rsaKey));
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);

String accessToken = ...

JwsHeader jwsHeader = JwsHeader.with(SignatureAlgorithm.RS256)
		.type("dpop+jwt")
		.jwk(rsaKey.toPublicJWK().toJSONObject())
		.build();
JwtClaimsSet claims = JwtClaimsSet.builder()
		.issuedAt(Instant.now())
		.claim("htm", "GET")
		.claim("htu", "https://resource.example.com/resource")
		.claim("ath", sha256(accessToken))
		.id(UUID.randomUUID().toString())
		.build();

Jwt dPoPProof = jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims));