对于最新的稳定版本,请使用 Spring Security 7.0.4spring-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(bearer token)形成对比,后者可被任何持有该访问Tokens的客户端使用。spring-doc.cadn.net.cn

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

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

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

资源服务器获取与访问Tokens绑定的公钥信息,该信息要么直接包含在访问Tokens中(JWT),要么通过Tokens自省(introspection)端点获取。 然后,资源服务器验证访问Tokens所绑定的公钥是否与 DPoP 证明中的公钥相匹配。 它还会验证 DPoP 证明中的访问Tokens哈希值是否与请求中的访问Tokens一致。spring-doc.cadn.net.cn

DPoP 访问Tokens请求

为了请求一个通过 DPoP 绑定到公钥的访问Tokens,客户端在向授权服务器的Tokens端点发起访问Tokens请求时,必须在 DPoP 请求头中提供有效的 DPoP 证明。 这适用于所有类型的访问Tokens请求,无论其授权许可类型如何(例如 authorization_coderefresh_tokenclient_credentials 等)。spring-doc.cadn.net.cn

以下 HTTP 请求展示了在 authorization_code 请求头中包含 DPoP 证明的 DPoP 访问Tokens请求: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 Proof 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_type 参数显示为 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,并验证该Tokens与 DPoP 证明中公钥的绑定关系。 这种绑定通过将公钥以资源服务器可访问的方式与访问Tokens关联来实现,例如直接将公钥哈希嵌入访问Tokens(如 JWT)中,或通过Tokens内省(token introspection)实现。spring-doc.cadn.net.cn

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

以下示例展示了包含 cnf 声明的 JWT 访问Tokens,其中 jkt 声明内又包含一个 2 声明,该声明表示 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 证明必须包含 ath 声明,其值为访问Tokens的有效哈希值。 资源服务器将计算所接收访问Tokens的哈希值,并验证其是否与 DPoP 证明中的 ath 声明一致。spring-doc.cadn.net.cn

DPoP 绑定的访问Tokens通过 Authorization 请求头发送,其认证方案为 DPoPspring-doc.cadn.net.cn

以下 HTTP 请求展示了在 Authorization 头中携带 DPoP 绑定的访问Tokens、并在 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

以下展示了包含 ath 声明的 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": "GET",
  "htu": "https://resource.example.com/resource",
  "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo",
  "iat": 1746807138,
  "jti": "3c2ee9bb-03ac-40cf-b812-00bfba341cee"
}

以下代码展示了如何生成 DPoP Proof 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));