From e3c39f02bcfd8bca00d6417dfe404cdfbcedb940 Mon Sep 17 00:00:00 2001 From: Joe Grandja <10884212+jgrandja@users.noreply.github.com> Date: Wed, 7 May 2025 14:09:23 -0400 Subject: [PATCH] Add documentation for DPoP support Closes gh-17072 --- .../DPoPAuthenticationConfigurer.java | 5 + docs/modules/ROOT/nav.adoc | 1 + .../oauth2/resource-server/dpop-tokens.adoc | 212 ++++++++++++++++++ docs/modules/ROOT/pages/whats-new.adoc | 1 + .../security/oauth2/jwt/DPoPProofContext.java | 57 +++++ .../jwt/DPoPProofJwtDecoderFactory.java | 24 +- .../DPoPAuthenticationProvider.java | 17 ++ .../DPoPAuthenticationToken.java | 30 +++ 8 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/resource-server/dpop-tokens.adoc diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java index b433602e5a..cee89e0427 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java @@ -53,9 +53,14 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** + * An {@link AbstractHttpConfigurer} for OAuth 2.0 Demonstrating Proof of Possession + * (DPoP) support. + * * @author Joe Grandja * @since 6.5 * @see DPoPAuthenticationProvider + * @see RFC 9449 + * OAuth 2.0 Demonstrating Proof of Possession (DPoP) */ final class DPoPAuthenticationConfigurer> extends AbstractHttpConfigurer, B> { diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d65fc977c0..e871974c12 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -82,6 +82,7 @@ **** xref:servlet/oauth2/resource-server/opaque-token.adoc[Opaque Token] **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy] **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens] +**** xref:servlet/oauth2/resource-server/dpop-tokens.adoc[DPoP-bound Access Tokens] ** xref:servlet/saml2/index.adoc[SAML2] *** xref:servlet/saml2/login/index.adoc[SAML2 Log In] **** xref:servlet/saml2/login/overview.adoc[SAML2 Log In Overview] diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/dpop-tokens.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/dpop-tokens.adoc new file mode 100644 index 0000000000..bf749baa72 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/dpop-tokens.adoc @@ -0,0 +1,212 @@ +[[oauth2-dpop-bound-access-tokens]] += OAuth 2.0 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 <>, 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 <>, 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 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. + +[[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 authorization server 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: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +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 +} +---- + +[[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" + } +} +---- + +[[dpop-protected-resource-request]] +== DPoP Protected Resource Request + +Requests to DPoP-protected resources MUST include both a DPoP proof and the DPoP-bound access token. +The DPoP proof MUST include the `ath` claim with a valid hash of the access token. +The resource server will calculate the hash of the received access token and verify that it is the same as the `ath` claim in the DPoP proof. + +A DPoP-bound access token is sent using the `Authorization` request header with an authentication scheme of `DPoP`. + +The following HTTP request shows a protected resource request with a DPoP-bound access token in the `Authorization` header and the DPoP proof in the `DPoP` header: + +[source,shell] +---- +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 +---- + +The following shows a representation of the DPoP Proof JWT header and claims with the `ath` claim: + +[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": "GET", + "htu": "https://resource.example.com/resource", + "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo", + "iat": 1746807138, + "jti": "3c2ee9bb-03ac-40cf-b812-00bfba341cee" +} +---- + +The following code shows an example of how to generate the DPoP Proof JWT: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +RSAKey rsaKey = ... +JWKSource 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)); +---- +====== diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index b1bc5f72b1..a75c999dd4 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -7,6 +7,7 @@ Below are the highlights of the release, or you can view https://github.com/spri == New Features * Support for automatic context-propagation with Micrometer (https://github.com/spring-projects/spring-security/issues/16665[gh-16665]) +* OAuth 2.0 Demonstrating Proof of Possession (DPoP) (https://github.com/spring-projects/spring-security/pull/16574[gh-16574]) == Breaking Changes diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java index 16a5947cf5..3a3fac8124 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java @@ -23,6 +23,9 @@ import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.util.Assert; /** + * A context class that holds a DPoP Proof {@link Jwt} and additional parameters + * associated to an Access Token request or a Protected Resource request. + * * @author Joe Grandja * @since 6.5 * @see DPoPProofJwtDecoderFactory @@ -44,28 +47,58 @@ public final class DPoPProofContext { this.accessToken = accessToken; } + /** + * Returns the DPoP Proof {@link Jwt}. + * @return the DPoP Proof {@link Jwt} + */ public String getDPoPProof() { return this.dPoPProof; } + /** + * Returns the value of the HTTP method of the request to which the DPoP Proof + * {@link Jwt} is attached. + * @return the value of the HTTP method of the request to which the DPoP Proof + * {@link Jwt} is attached + */ public String getMethod() { return this.method; } + /** + * Returns the value of the HTTP target URI of the request to which the DPoP Proof + * {@link Jwt} is attached, without query and fragment parts. + * @return the value of the HTTP target URI of the request to which the DPoP Proof + * {@link Jwt} is attached + */ public String getTargetUri() { return this.targetUri; } + /** + * Returns the access token if the request is a Protected Resource request. + * @param the type of the access token + * @return the access token if the request is a Protected Resource request or + * {@code null} + */ @SuppressWarnings("unchecked") @Nullable public T getAccessToken() { return (T) this.accessToken; } + /** + * Returns a new {@link Builder}, initialized with the DPoP Proof {@link Jwt}. + * @param dPoPProof the DPoP Proof {@link Jwt} + * @return the {@link Builder} + */ public static Builder withDPoPProof(String dPoPProof) { return new Builder(dPoPProof); } + /** + * A builder for {@link DPoPProofContext}. + */ public static final class Builder { private String dPoPProof; @@ -81,21 +114,45 @@ public final class DPoPProofContext { this.dPoPProof = dPoPProof; } + /** + * Sets the value of the HTTP method of the request to which the DPoP Proof + * {@link Jwt} is attached. + * @param method the value of the HTTP method of the request to which the DPoP + * Proof {@link Jwt} is attached + * @return the {@link Builder} + */ public Builder method(String method) { this.method = method; return this; } + /** + * Sets the value of the HTTP target URI of the request to which the DPoP Proof + * {@link Jwt} is attached, without query and fragment parts. + * @param targetUri the value of the HTTP target URI of the request to which the + * DPoP Proof {@link Jwt} is attached + * @return the {@link Builder} + */ public Builder targetUri(String targetUri) { this.targetUri = targetUri; return this; } + /** + * Sets the access token if the request is a Protected Resource request. + * @param accessToken the access token if the request is a Protected Resource + * request + * @return the {@link Builder} + */ public Builder accessToken(OAuth2Token accessToken) { this.accessToken = accessToken; return this; } + /** + * Builds a new {@link DPoPProofContext}. + * @return a {@link DPoPProofContext} + */ public DPoPProofContext build() { validate(); return new DPoPProofContext(this.dPoPProof, this.method, this.targetUri, this.accessToken); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofJwtDecoderFactory.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofJwtDecoderFactory.java index 9b27313e37..509d0d25eb 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofJwtDecoderFactory.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofJwtDecoderFactory.java @@ -48,17 +48,29 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** + * A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder} for the + * specified {@link DPoPProofContext} and is used for authenticating a DPoP Proof + * {@link Jwt}. + * * @author Joe Grandja * @since 6.5 + * @see JwtDecoderFactory * @see DPoPProofContext + * @see RFC 9449 + * OAuth 2.0 Demonstrating Proof of Possession (DPoP) */ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory { + /** + * The default {@code OAuth2TokenValidator} factory that validates the + * {@code htm}, {@code htu}, {@code jti} and {@code iat} claims of the DPoP Proof + * {@link Jwt}. + */ + public static final Function> DEFAULT_JWT_VALIDATOR_FACTORY = defaultJwtValidatorFactory(); + private static final JOSEObjectTypeVerifier DPOP_TYPE_VERIFIER = new DefaultJOSEObjectTypeVerifier<>( new JOSEObjectType("dpop+jwt")); - public static final Function> DEFAULT_JWT_VALIDATOR_FACTORY = defaultJwtValidatorFactory(); - private Function> jwtValidatorFactory = DEFAULT_JWT_VALIDATOR_FACTORY; @Override @@ -69,6 +81,14 @@ public final class DPoPProofJwtDecoderFactory implements JwtDecoderFactory} factory is + * {@link #DEFAULT_JWT_VALIDATOR_FACTORY}. + * @param jwtValidatorFactory the factory that provides an + * {@link OAuth2TokenValidator} for the specified {@link DPoPProofContext} + */ public void setJwtValidatorFactory(Function> jwtValidatorFactory) { Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null"); this.jwtValidatorFactory = jwtValidatorFactory; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java index b26cb754c7..32d7b09fa1 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java @@ -50,10 +50,15 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** + * An {@link AuthenticationProvider} implementation that is responsible for authenticating + * a DPoP-bound access token for a protected resource request. + * * @author Joe Grandja * @since 6.5 * @see DPoPAuthenticationToken * @see DPoPProofJwtDecoderFactory + * @see RFC 9449 + * OAuth 2.0 Demonstrating Proof of Possession (DPoP) */ public final class DPoPAuthenticationProvider implements AuthenticationProvider { @@ -61,6 +66,11 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider private JwtDecoderFactory dPoPProofVerifierFactory; + /** + * Constructs a {@code DPoPAuthenticationProvider} using the provided parameters. + * @param tokenAuthenticationManager the {@link AuthenticationManager} used to + * authenticate the DPoP-bound access token + */ public DPoPAuthenticationProvider(AuthenticationManager tokenAuthenticationManager) { Assert.notNull(tokenAuthenticationManager, "tokenAuthenticationManager cannot be null"); this.tokenAuthenticationManager = tokenAuthenticationManager; @@ -121,6 +131,13 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider return DPoPAuthenticationToken.class.isAssignableFrom(authentication); } + /** + * Sets the {@link JwtDecoderFactory} that provides a {@link JwtDecoder} for the + * specified {@link DPoPProofContext} and is used for authenticating a DPoP Proof + * {@link Jwt}. The default factory is {@link DPoPProofJwtDecoderFactory}. + * @param dPoPProofVerifierFactory the {@link JwtDecoderFactory} that provides a + * {@link JwtDecoder} for the specified {@link DPoPProofContext} + */ public void setDPoPProofVerifierFactory(JwtDecoderFactory dPoPProofVerifierFactory) { Assert.notNull(dPoPProofVerifierFactory, "dPoPProofVerifierFactory cannot be null"); this.dPoPProofVerifierFactory = dPoPProofVerifierFactory; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationToken.java index 0abca69706..35593d8361 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationToken.java @@ -20,9 +20,14 @@ import java.io.Serial; import java.util.Collections; import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.util.Assert; /** + * An {@link Authentication} representing a protected resource request with a DPoP-bound + * access token. + * * @author Joe Grandja * @since 6.5 * @see DPoPAuthenticationProvider @@ -40,6 +45,14 @@ public class DPoPAuthenticationToken extends AbstractAuthenticationToken { private final String resourceUri; + /** + * Constructs a {@code DPoPAuthenticationToken} using the provided parameters. + * @param accessToken the DPoP-bound access token + * @param dPoPProof the DPoP Proof {@link Jwt} + * @param method the value of the HTTP method of the request + * @param resourceUri the value of the HTTP resource URI of the request, without query + * and fragment parts + */ public DPoPAuthenticationToken(String accessToken, String dPoPProof, String method, String resourceUri) { super(Collections.emptyList()); Assert.hasText(accessToken, "accessToken cannot be empty"); @@ -62,18 +75,35 @@ public class DPoPAuthenticationToken extends AbstractAuthenticationToken { return getAccessToken(); } + /** + * Returns the DPoP-bound access token. + * @return the DPoP-bound access token + */ public String getAccessToken() { return this.accessToken; } + /** + * Returns the DPoP Proof {@link Jwt}. + * @return the DPoP Proof {@link Jwt} + */ public String getDPoPProof() { return this.dPoPProof; } + /** + * Returns the value of the HTTP method of the request. + * @return the value of the HTTP method of the request + */ public String getMethod() { return this.method; } + /** + * Returns the value of the HTTP resource URI of the request, without query and + * fragment parts. + * @return the value of the HTTP resource URI of the request + */ public String getResourceUri() { return this.resourceUri; }