Browse Source

Add documentation for DPoP support

Closes gh-2009
pull/2011/head
Joe Grandja 7 months ago
parent
commit
3debeb6f65
  1. 6
      docs/modules/ROOT/pages/overview.adoc
  2. 132
      docs/modules/ROOT/pages/protocol-endpoints.adoc
  3. 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/DPoPProofVerifier.java
  4. 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2TokenContext.java

6
docs/modules/ROOT/pages/overview.adoc

@ -58,6 +58,12 @@ Spring Authorization Server supports the following features: @@ -58,6 +58,12 @@ Spring Authorization Server supports the following features:
* JSON Web Token (JWT) (https://tools.ietf.org/html/rfc7519[RFC 7519])
* JSON Web Signature (JWS) (https://tools.ietf.org/html/rfc7515[RFC 7515])
|Token Types
|
* xref:protocol-endpoints.adoc#oauth2-token-endpoint-dpop-bound-access-tokens[DPoP-bound Access Tokens]
|
* OAuth 2.0 Demonstrating Proof of Possession (DPoP) (https://datatracker.ietf.org/doc/html/rfc9449[RFC 9449])
|xref:configuration-model.adoc#configuring-client-authentication[Client Authentication]
|
* `client_secret_basic`

132
docs/modules/ROOT/pages/protocol-endpoints.adoc

@ -349,6 +349,138 @@ static class CustomScopeValidator implements Consumer<OAuth2ClientCredentialsAut @@ -349,6 +349,138 @@ static class CustomScopeValidator implements Consumer<OAuth2ClientCredentialsAut
}
----
[[oauth2-token-endpoint-dpop-bound-access-tokens]]
=== DPoP-bound Access Tokens
https://datatracker.ietf.org/doc/html/rfc9449[RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)] is an application-level mechanism for sender-constraining an access token.
The primary goal of DPoP is to prevent unauthorized or illegitimate clients from using leaked or stolen access tokens, by binding an access token to a public key upon issuance by the authorization server and requiring that the client proves possession of the corresponding private key when using the access token at the resource server.
Access tokens that are sender-constrained via DPoP stand in contrast to the typical bearer token, which can be used by any client in possession of the access token.
DPoP introduces the concept of a https://datatracker.ietf.org/doc/html/rfc9449#name-dpop-proof-jwts[DPoP Proof], which is a JWT created by the client and sent as a header in an HTTP request.
A client uses a DPoP proof to prove the possession of a private key corresponding to a certain public key.
When the client initiates an access token request, it attaches a DPoP proof to the request in an HTTP header.
The authorization server binds (sender-constrains) the access token to the public key associated in the DPoP proof.
When the client initiates a protected resource request, it again attaches a DPoP proof to the request in an HTTP header.
The resource server obtains information about the public key bound to the access token, either directly in the access token (JWT) or via the <<oauth2-token-introspection-endpoint,OAuth2 Token Introspection endpoint>>.
The resource server then verifies that the public key bound to the access token matches the public key in the DPoP proof.
It also verifies that the access token hash in the DPoP proof matches the access token in the request.
[[oauth2-token-endpoint-dpop-access-token-request]]
==== DPoP Access Token Request
To request an access token that is bound to a public key using DPoP, the client MUST provide a valid DPoP proof in the `DPoP` header when making an access token request to the OAuth2 Token endpoint.
This is applicable for all access token requests regardless of authorization grant type (e.g. `authorization_code`, `refresh_token`, `client_credentials`, etc).
The following HTTP request shows an `authorization_code` access token request with a DPoP proof in the `DPoP` header:
[source,shell]
----
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-
----
The following shows a representation of the DPoP Proof JWT header and claims:
[source,json]
----
{
"typ": "dpop+jwt",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"n": "3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw"
}
}
----
[source,json]
----
{
"htm": "POST",
"htu": "https://server.example.com/oauth2/token",
"iat": 1746806305,
"jti": "4b2340d2-a91f-40a5-baa9-dd4e5deac867"
}
----
The following code shows an example of how to generate the DPoP Proof JWT:
[source,java]
----
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));
----
After the authorization server successfully validates the DPoP proof, the public key from the DPoP proof will be bound (sender-constrained) to the issued access token.
The following access token response shows the `token_type` parameter as `DPoP` to signal to the client that the access token was bound to its DPoP proof public key:
[source,shell]
----
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
}
----
[[oauth2-token-endpoint-dpop-public-key-confirmation]]
==== Public Key Confirmation
Resource servers MUST be able to identify whether an access token is DPoP-bound and verify the binding to the public key of the DPoP proof.
The binding is accomplished by associating the public key with the access token in a way that can be accessed by the resource server, such as embedding the public key hash in the access token directly (JWT) or through token introspection.
When an access token is represented as a JWT, the public key hash is contained in the `jkt` claim under the confirmation method (`cnf`) claim.
The following example shows the claims of a JWT access token containing a `cnf` claim with a `jkt` claim, which is the JWK SHA-256 Thumbprint of the DPoP proof public key:
[source,json]
----
{
"sub":"user@example.com",
"iss":"https://server.example.com",
"nbf":1562262611,
"exp":1562266216,
"cnf":
{
"jkt":"CQMknzRoZ5YUi7vS58jck1q8TmZT8wiIiXrCN1Ny4VU"
}
}
----
[[oauth2-token-introspection-endpoint]]
== OAuth2 Token Introspection Endpoint

5
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/DPoPProofVerifier.java

@ -26,8 +26,13 @@ import org.springframework.security.oauth2.jwt.JwtDecoderFactory; @@ -26,8 +26,13 @@ import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.util.StringUtils;
/**
* A verifier for DPoP Proof {@link Jwt}'s.
*
* @author Joe Grandja
* @since 1.5
* @see DPoPProofJwtDecoderFactory
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449
* OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
*/
final class DPoPProofVerifier {

1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2TokenContext.java

@ -45,6 +45,7 @@ import org.springframework.util.Assert; @@ -45,6 +45,7 @@ import org.springframework.util.Assert;
public interface OAuth2TokenContext extends Context {
/**
* The key used for the DPoP Proof {@link Jwt} (if available).
* @since 1.5
*/
String DPOP_PROOF_KEY = Jwt.class.getName().concat(".DPOP_PROOF");

Loading…
Cancel
Save