From 3debeb6f652dec995ccf2e9856ae2952cfee76f0 Mon Sep 17 00:00:00 2001 From: Joe Grandja <10884212+jgrandja@users.noreply.github.com> Date: Wed, 14 May 2025 06:45:16 -0400 Subject: [PATCH] Add documentation for DPoP support Closes gh-2009 --- docs/modules/ROOT/pages/overview.adoc | 6 + .../ROOT/pages/protocol-endpoints.adoc | 132 ++++++++++++++++++ .../authentication/DPoPProofVerifier.java | 5 + .../token/OAuth2TokenContext.java | 1 + 4 files changed, 144 insertions(+) diff --git a/docs/modules/ROOT/pages/overview.adoc b/docs/modules/ROOT/pages/overview.adoc index b75bf835..07887993 100644 --- a/docs/modules/ROOT/pages/overview.adoc +++ b/docs/modules/ROOT/pages/overview.adoc @@ -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` diff --git a/docs/modules/ROOT/pages/protocol-endpoints.adoc b/docs/modules/ROOT/pages/protocol-endpoints.adoc index 72e5d938..95a31441 100644 --- a/docs/modules/ROOT/pages/protocol-endpoints.adoc +++ b/docs/modules/ROOT/pages/protocol-endpoints.adoc @@ -349,6 +349,138 @@ static class CustomScopeValidator implements Consumer>. +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 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 diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/DPoPProofVerifier.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/DPoPProofVerifier.java index 161eaf23..a48592da 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/DPoPProofVerifier.java +++ b/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; import org.springframework.util.StringUtils; /** + * A verifier for DPoP Proof {@link Jwt}'s. + * * @author Joe Grandja * @since 1.5 + * @see DPoPProofJwtDecoderFactory + * @see RFC 9449 + * OAuth 2.0 Demonstrating Proof of Possession (DPoP) */ final class DPoPProofVerifier { diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2TokenContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2TokenContext.java index 9ab64686..344fc410 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2TokenContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/token/OAuth2TokenContext.java @@ -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");