From f8359ef61909af2d04a7637195eaa9d2d33e7fce Mon Sep 17 00:00:00 2001
From: Joe Grandja <10884212+jgrandja@users.noreply.github.com>
Date: Wed, 8 Apr 2026 11:06:46 -0400
Subject: [PATCH] Polish gh-17202
---
.../DPoPAuthenticationConfigurer.java | 169 ---------------
.../OAuth2ResourceServerConfigurer.java | 202 ++++++++++++++++--
.../annotation/web/OAuth2ResourceServerDsl.kt | 24 +--
.../web/oauth2/resourceserver/DPoPDsl.kt | 41 ++--
.../security/config/spring-security-7.0.rnc | 13 --
.../security/config/spring-security-7.0.xsd | 20 --
...ests.java => DPoPAuthenticationTests.java} | 78 +------
.../web/oauth2/resourceserver/DPoPDslTests.kt | 6 +-
.../web/DPoPAuthenticationEntryPoint.java | 10 +
.../resource/web/DPoPRequestMatcher.java | 37 ----
.../DPoPAuthenticationConverter.java | 14 +-
11 files changed, 251 insertions(+), 363 deletions(-)
delete mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java
rename config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/{DPoPAuthenticationConfigurerTests.java => DPoPAuthenticationTests.java} (80%)
delete mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPRequestMatcher.java
rename oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/{ => web}/authentication/DPoPAuthenticationConverter.java (85%)
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
deleted file mode 100644
index a3cafd44b5..0000000000
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2004-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource;
-
-import jakarta.servlet.http.HttpServletRequest;
-
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.AuthenticationManagerResolver;
-import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
-import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
-import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationConverter;
-import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
-import org.springframework.security.oauth2.server.resource.web.DPoPAuthenticationEntryPoint;
-import org.springframework.security.oauth2.server.resource.web.DPoPRequestMatcher;
-import org.springframework.security.web.authentication.AuthenticationConverter;
-import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
-import org.springframework.security.web.authentication.AuthenticationFailureHandler;
-import org.springframework.security.web.authentication.AuthenticationFilter;
-import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
-import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
-import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.springframework.util.Assert;
-import org.springframework.web.context.request.RequestAttributes;
-import org.springframework.web.context.request.RequestContextHolder;
-import org.springframework.web.context.request.ServletRequestAttributes;
-
-/**
- * An {@link AbstractHttpConfigurer} for OAuth 2.0 Demonstrating Proof of Possession
- * (DPoP) support.
- *
- * @author Joe Grandja
- * @author Max Batischev
- * @since 6.5
- * @see DPoPAuthenticationProvider
- * @see RFC 9449
- * OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- */
-public final class DPoPAuthenticationConfigurer>
- extends AbstractHttpConfigurer, B> {
-
- private RequestMatcher requestMatcher;
-
- private AuthenticationConverter authenticationConverter;
-
- private AuthenticationSuccessHandler authenticationSuccessHandler;
-
- private AuthenticationFailureHandler authenticationFailureHandler;
-
- @Override
- public void configure(B http) {
- AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
- http.authenticationProvider(new DPoPAuthenticationProvider(getTokenAuthenticationManager(http)));
- AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager,
- getAuthenticationConverter());
- authenticationFilter.setRequestMatcher(getRequestMatcher());
- authenticationFilter.setSuccessHandler(getAuthenticationSuccessHandler());
- authenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
- authenticationFilter.setSecurityContextRepository(new RequestAttributeSecurityContextRepository());
- authenticationFilter = postProcess(authenticationFilter);
- http.addFilter(authenticationFilter);
- }
-
- private AuthenticationManager getTokenAuthenticationManager(B http) {
- OAuth2ResourceServerConfigurer resourceServerConfigurer = http
- .getConfigurer(OAuth2ResourceServerConfigurer.class);
- final AuthenticationManagerResolver authenticationManagerResolver = resourceServerConfigurer
- .getAuthenticationManagerResolver();
- if (authenticationManagerResolver == null) {
- return resourceServerConfigurer.getAuthenticationManager(http);
- }
- return (authentication) -> {
- RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
- ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
- AuthenticationManager authenticationManager = authenticationManagerResolver
- .resolve(servletRequestAttributes.getRequest());
- return authenticationManager.authenticate(authentication);
- };
- }
-
- /**
- * Sets the {@link RequestMatcher} to use.
- * @param requestMatcher
- * @since 7.0
- */
- public DPoPAuthenticationConfigurer requestMatcher(RequestMatcher requestMatcher) {
- Assert.notNull(requestMatcher, "requestMatcher cannot be null");
- this.requestMatcher = requestMatcher;
- return this;
- }
-
- /**
- * Sets the {@link AuthenticationConverter} to use.
- * @param authenticationConverter
- * @since 7.0
- */
- public DPoPAuthenticationConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) {
- Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
- this.authenticationConverter = authenticationConverter;
- return this;
- }
-
- /**
- * Sets the {@link AuthenticationFailureHandler} to use.
- * @param failureHandler
- * @since 7.0
- */
- public DPoPAuthenticationConfigurer failureHandler(AuthenticationFailureHandler failureHandler) {
- Assert.notNull(failureHandler, "failureHandler cannot be null");
- this.authenticationFailureHandler = failureHandler;
- return this;
- }
-
- /**
- * Sets the {@link AuthenticationSuccessHandler} to use.
- * @param successHandler
- * @since 7.0
- */
- public DPoPAuthenticationConfigurer successHandler(AuthenticationSuccessHandler successHandler) {
- Assert.notNull(successHandler, "successHandler cannot be null");
- this.authenticationSuccessHandler = successHandler;
- return this;
- }
-
- private RequestMatcher getRequestMatcher() {
- if (this.requestMatcher == null) {
- this.requestMatcher = new DPoPRequestMatcher();
- }
- return this.requestMatcher;
- }
-
- private AuthenticationConverter getAuthenticationConverter() {
- if (this.authenticationConverter == null) {
- this.authenticationConverter = new DPoPAuthenticationConverter();
- }
- return this.authenticationConverter;
- }
-
- private AuthenticationSuccessHandler getAuthenticationSuccessHandler() {
- if (this.authenticationSuccessHandler == null) {
- this.authenticationSuccessHandler = (request, response, authentication) -> {
- // No-op - will continue on filter chain
- };
- }
- return this.authenticationSuccessHandler;
- }
-
- private AuthenticationFailureHandler getAuthenticationFailureHandler() {
- if (this.authenticationFailureHandler == null) {
- this.authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(
- new DPoPAuthenticationEntryPoint());
- }
- return this.authenticationFailureHandler;
- }
-
-}
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
index ef1ad2d499..7cb3e224b1 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
@@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
@@ -40,11 +41,14 @@ import org.springframework.security.config.annotation.web.configurers.ExceptionH
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.FactorGrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.OAuth2ProtectedResourceMetadata;
+import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationProvider;
+import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
@@ -53,16 +57,23 @@ import org.springframework.security.oauth2.server.resource.introspection.OpaqueT
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
+import org.springframework.security.oauth2.server.resource.web.DPoPAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.OAuth2ProtectedResourceMetadataFilter;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
+import org.springframework.security.oauth2.server.resource.web.authentication.DPoPAuthenticationConverter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationFilter;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
@@ -72,8 +83,12 @@ import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
/**
*
@@ -146,6 +161,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy;
* @author Josh Cummings
* @author Evgeniy Cheban
* @author Jerome Wacongne <ch4mp@c4-soft.com>
+ * @author Joe Grandja
* @author Max Batischev
* @since 5.1
* @see BearerTokenAuthenticationFilter
@@ -169,8 +185,6 @@ public final class OAuth2ResourceServerConfigurer dPoPAuthenticationConfigurer;
-
private AuthenticationManagerResolver authenticationManagerResolver;
private AuthenticationConverter authenticationConverter;
@@ -179,6 +193,8 @@ public final class OAuth2ResourceServerConfigurer dPoP(Customizer dPoPCustomizer) {
+ if (this.dPoPConfigurer == null) {
+ this.dPoPConfigurer = new DPoPConfigurer();
+ }
+ dPoPCustomizer.customize(this.dPoPConfigurer);
+ return this;
+ }
+
/**
* Configure OAuth 2.0 Protected Resource Metadata.
* @param protectedResourceMetadataCustomizer the {@link Customizer} to provide more
@@ -271,22 +302,6 @@ public final class OAuth2ResourceServerConfigurer dpop(
- Customizer> dpopAuthenticatioCustomizer) {
- if (this.dPoPAuthenticationConfigurer == null) {
- this.dPoPAuthenticationConfigurer = new DPoPAuthenticationConfigurer<>();
- }
- dpopAuthenticatioCustomizer.customize(this.dPoPAuthenticationConfigurer);
- return this;
- }
-
@Override
public void init(H http) {
validateConfiguration();
@@ -315,8 +330,8 @@ public final class OAuth2ResourceServerConfigurerRFC
+ * 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)
+ */
+ public final class DPoPConfigurer {
+
+ private RequestMatcher requestMatcher;
+
+ private AuthenticationConverter authenticationConverter;
+
+ private AuthenticationSuccessHandler authenticationSuccessHandler;
+
+ private AuthenticationFailureHandler authenticationFailureHandler;
+
+ /**
+ * Sets the {@link RequestMatcher} used when matching the
+ * {@link HttpServletRequest} to a DPoP-protected resource request.
+ * @param requestMatcher the {@link RequestMatcher} used when matching the
+ * {@link HttpServletRequest} to a DPoP-protected resource request
+ * @return the {@link DPoPConfigurer} for further configuration
+ */
+ public DPoPConfigurer requestMatcher(RequestMatcher requestMatcher) {
+ Assert.notNull(requestMatcher, "requestMatcher cannot be null");
+ this.requestMatcher = requestMatcher;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AuthenticationConverter} used when attempting to extract a
+ * DPoP-bound access token from {@link HttpServletRequest} to an instance of
+ * {@link DPoPAuthenticationToken} used for authenticating the DPoP-protected
+ * resource request. The default is {@link DPoPAuthenticationConverter}.
+ * @param authenticationConverter the {@link AuthenticationConverter} used when
+ * attempting to extract a DPoP-bound access token from {@link HttpServletRequest}
+ * @return the {@link DPoPConfigurer} for further configuration
+ */
+ public DPoPConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) {
+ Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+ this.authenticationConverter = authenticationConverter;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AuthenticationSuccessHandler} used for handling an
+ * authenticated DPoP-protected resource request.
+ * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler}
+ * used for handling an authenticated DPoP-protected resource request
+ * @return the {@link DPoPConfigurer} for further configuration
+ */
+ public DPoPConfigurer authenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
+ Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
+ this.authenticationSuccessHandler = authenticationSuccessHandler;
+ return this;
+ }
+
+ /**
+ * Sets the {@link AuthenticationFailureHandler} used for handling a failed
+ * DPoP-protected resource request. The default is
+ * {@link AuthenticationEntryPointFailureHandler} with
+ * {@link DPoPAuthenticationEntryPoint}.
+ * @param authenticationFailureHandler the {@link AuthenticationFailureHandler}
+ * used for handling a failed DPoP-protected resource request
+ * @return the {@link DPoPConfigurer} for further configuration
+ */
+ public DPoPConfigurer authenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
+ Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
+ this.authenticationFailureHandler = authenticationFailureHandler;
+ return this;
+ }
+
+ private void configure(H http) {
+ AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
+ http.authenticationProvider(new DPoPAuthenticationProvider(getTokenAuthenticationManager(http)));
+ AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager,
+ getAuthenticationConverter());
+ authenticationFilter.setRequestMatcher(getRequestMatcher());
+ authenticationFilter.setSuccessHandler(getAuthenticationSuccessHandler());
+ authenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
+ authenticationFilter.setSecurityContextRepository(new RequestAttributeSecurityContextRepository());
+ authenticationFilter = postProcess(authenticationFilter);
+ http.addFilter(authenticationFilter);
+ }
+
+ private AuthenticationManager getTokenAuthenticationManager(H http) {
+ final AuthenticationManagerResolver authenticationManagerResolver = getAuthenticationManagerResolver();
+ if (authenticationManagerResolver == null) {
+ return getAuthenticationManager(http);
+ }
+ return (authentication) -> {
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
+ AuthenticationManager authenticationManager = authenticationManagerResolver
+ .resolve(servletRequestAttributes.getRequest());
+ return authenticationManager.authenticate(authentication);
+ };
+ }
+
+ private RequestMatcher getRequestMatcher() {
+ if (this.requestMatcher == null) {
+ this.requestMatcher = this::matchesDPoPRequest;
+ }
+ return this.requestMatcher;
+ }
+
+ private AuthenticationConverter getAuthenticationConverter() {
+ if (this.authenticationConverter == null) {
+ this.authenticationConverter = new DPoPAuthenticationConverter();
+ }
+ return this.authenticationConverter;
+ }
+
+ private AuthenticationSuccessHandler getAuthenticationSuccessHandler() {
+ if (this.authenticationSuccessHandler == null) {
+ this.authenticationSuccessHandler = (request, response, authentication) -> {
+ // No-op - will continue on filter chain
+ };
+ }
+ return this.authenticationSuccessHandler;
+ }
+
+ private AuthenticationFailureHandler getAuthenticationFailureHandler() {
+ if (this.authenticationFailureHandler == null) {
+ this.authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(
+ new DPoPAuthenticationEntryPoint());
+ }
+ return this.authenticationFailureHandler;
+ }
+
+ private boolean matchesDPoPRequest(HttpServletRequest request) {
+ String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
+ if (!StringUtils.hasText(authorization)) {
+ return false;
+ }
+ return StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue());
+ }
+
+ }
+
public static final class ProtectedResourceMetadataConfigurer {
private Consumer protectedResourceMetadataCustomizer;
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDsl.kt
index cab26596d7..dc1f945bb8 100644
--- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDsl.kt
@@ -16,17 +16,16 @@
package org.springframework.security.config.annotation.web
+import jakarta.servlet.http.HttpServletRequest
import org.springframework.security.authentication.AuthenticationManagerResolver
import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
+import org.springframework.security.config.annotation.web.oauth2.resourceserver.DPoPDsl
import org.springframework.security.config.annotation.web.oauth2.resourceserver.JwtDsl
import org.springframework.security.config.annotation.web.oauth2.resourceserver.OpaqueTokenDsl
-import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.access.AccessDeniedHandler
-import jakarta.servlet.http.HttpServletRequest
-import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.DPoPAuthenticationConfigurer
-import org.springframework.security.config.annotation.web.oauth2.resourceserver.DPoPDsl
/**
* A Kotlin DSL to configure [HttpSecurity] OAuth 2.0 resource server support using
@@ -51,7 +50,7 @@ class OAuth2ResourceServerDsl {
private var jwt: ((OAuth2ResourceServerConfigurer.JwtConfigurer) -> Unit)? = null
private var opaqueToken: ((OAuth2ResourceServerConfigurer.OpaqueTokenConfigurer) -> Unit)? = null
- private var dpop: ((DPoPAuthenticationConfigurer) -> Unit)? = null
+ private var dPoP: ((OAuth2ResourceServerConfigurer.DPoPConfigurer) -> Unit)? = null
/**
* Enables JWT-encoded bearer token support.
@@ -114,7 +113,7 @@ class OAuth2ResourceServerDsl {
}
/**
- * Enables DPoP support.
+ * Enables DPoP-bound access token support.
*
* Example:
*
@@ -127,7 +126,8 @@ class OAuth2ResourceServerDsl {
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
* http {
* oauth2ResourceServer {
- * dpop { }
+ * jwt { }
+ * dPoP { }
* }
* }
* return http.build()
@@ -135,12 +135,12 @@ class OAuth2ResourceServerDsl {
* }
* ```
*
- * @param dpopConfig custom configurations to configure DPoP support
+ * @param dPoPConfig custom configurations to configure DPoP-bound access token support
* @see [DPoPDsl]
- * @since 7.0
+ * @since 7.1
*/
- fun dpop(dpopConfig: DPoPDsl.() -> Unit) {
- this.dpop = DPoPDsl().apply(dpopConfig).get()
+ fun dPoP(dPoPConfig: DPoPDsl.() -> Unit) {
+ this.dPoP = DPoPDsl().apply(dPoPConfig).get()
}
internal fun get(): (OAuth2ResourceServerConfigurer) -> Unit {
@@ -151,7 +151,7 @@ class OAuth2ResourceServerDsl {
authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver) }
jwt?.also { oauth2ResourceServer.jwt(jwt) }
opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) }
- dpop?.also { oauth2ResourceServer.dpop(dpop) }
+ dPoP?.also { oauth2ResourceServer.dPoP(dPoP) }
}
}
}
diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDsl.kt
index a31d362abc..a2d5591ab0 100644
--- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDsl.kt
+++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDsl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2025 the original author or authors.
+ * Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,35 +16,44 @@
package org.springframework.security.config.annotation.web.oauth2.resourceserver
+import jakarta.servlet.http.HttpServletRequest
import org.springframework.security.config.annotation.web.builders.HttpSecurity
-import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.DPoPAuthenticationConfigurer
+import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer
+import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken
+import org.springframework.security.oauth2.server.resource.web.DPoPAuthenticationEntryPoint
+import org.springframework.security.oauth2.server.resource.web.authentication.DPoPAuthenticationConverter
import org.springframework.security.web.authentication.AuthenticationConverter
+import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.util.matcher.RequestMatcher
/**
- * A Kotlin DSL to configure DPoP support using idiomatic Kotlin code.
+ * A Kotlin DSL to configure DPoP-bound access token support using idiomatic Kotlin code.
*
* @author Max Batischev
- * @property requestMatcher the [RequestMatcher] to use.
- * @property authenticationConverter the [AuthenticationConverter] to use.
- * @property successHandler the [AuthenticationSuccessHandler] to use.
- * @property failureHandler the [AuthenticationFailureHandler] to use.
- * @since 7.0
+ * @property requestMatcher the [RequestMatcher] used when matching the [HttpServletRequest] to a DPoP-protected resource request.
+ * @property authenticationConverter the [AuthenticationConverter] used when attempting to extract a DPoP-bound access token
+ * from [HttpServletRequest] to an instance of [DPoPAuthenticationToken] used for authenticating the DPoP-protected resource request.
+ * The default is [DPoPAuthenticationConverter].
+ * @property authenticationSuccessHandler the [AuthenticationSuccessHandler] used for handling an authenticated DPoP-protected resource request.
+ * @property authenticationFailureHandler the [AuthenticationFailureHandler] used for handling a failed DPoP-protected resource request.
+ * The default is [AuthenticationEntryPointFailureHandler] with [DPoPAuthenticationEntryPoint].
+ * @since 7.1
*/
+@OAuth2ResourceServerSecurityMarker
class DPoPDsl {
var requestMatcher: RequestMatcher? = null
var authenticationConverter: AuthenticationConverter? = null
- var successHandler: AuthenticationSuccessHandler? = null
- var failureHandler: AuthenticationFailureHandler? = null
+ var authenticationSuccessHandler: AuthenticationSuccessHandler? = null
+ var authenticationFailureHandler: AuthenticationFailureHandler? = null
- internal fun get(): (DPoPAuthenticationConfigurer) -> Unit {
- return { dpop ->
- requestMatcher?.also { dpop.requestMatcher(requestMatcher) }
- authenticationConverter?.also { dpop.authenticationConverter(authenticationConverter) }
- successHandler?.also { dpop.successHandler(successHandler) }
- failureHandler?.also { dpop.failureHandler(failureHandler) }
+ internal fun get(): (OAuth2ResourceServerConfigurer.DPoPConfigurer) -> Unit {
+ return { dPoP ->
+ requestMatcher?.also { dPoP.requestMatcher(requestMatcher) }
+ authenticationConverter?.also { dPoP.authenticationConverter(authenticationConverter) }
+ authenticationSuccessHandler?.also { dPoP.authenticationSuccessHandler(authenticationSuccessHandler) }
+ authenticationFailureHandler?.also { dPoP.authenticationFailureHandler(authenticationFailureHandler) }
}
}
}
diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc
index 8949bb6e4d..3ab50e1837 100644
--- a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc
+++ b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc
@@ -686,19 +686,6 @@ opaque-token.attlist &=
## Reference to an OpaqueTokenAuthenticationConverter responsible for converting successful introspection result into an Authentication.
attribute authentication-converter-ref {xsd:token}?
-dpop =
- ## Configuration DpoP
- element dpop {dpop.attlist}
-dpop.attlist &=
- ## DPoP Request Matcher
- attribute dpop-request-matcher-ref {xsd:token}?
-dpop.attlist &=
- attribute dpop-authentication-converter-ref {xsd:token}?
-dpop.attlist &=
- attribute dpop-success-handler-ref {xsd:token}?
-dpop.attlist &=
- attribute dpop-failure-handler-ref {xsd:token}?
-
saml2-login =
## Configures authentication support for SAML 2.0 Login
element saml2-login {saml2-login.attlist}
diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd
index d29198f422..fe0bfee559 100644
--- a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd
+++ b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd
@@ -2074,26 +2074,6 @@
-
-
- Configuration DpoP
-
-
-
-
-
-
-
-
-
- DPoP Request Matcher
-
-
-
-
-
-
-
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationTests.java
similarity index 80%
rename from config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurerTests.java
rename to config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationTests.java
index bfb2fddc3a..427494b673 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationTests.java
@@ -37,8 +37,6 @@ import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -53,7 +51,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
-import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.jose.TestKeys;
@@ -65,28 +62,24 @@ import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
- * Tests for {@link DPoPAuthenticationConfigurer}.
+ * Integration tests for OAuth 2.0 Demonstrating Proof of Possession (DPoP) support.
*
* @author Joe Grandja
*/
@ExtendWith(SpringTestContextExtension.class)
-public class DPoPAuthenticationConfigurerTests {
+public class DPoPAuthenticationTests {
private static final RSAPublicKey PROVIDER_RSA_PUBLIC_KEY = TestKeys.DEFAULT_PUBLIC_KEY;
@@ -183,22 +176,6 @@ public class DPoPAuthenticationConfigurerTests {
// @formatter:on
}
- @Test
- public void requestWhenCustomSuccessHandlerIsPresentThenAccessed() throws Exception {
- this.spring.register(SecurityConfigWithCustomSuccessHandler.class, ResourceEndpoints.class).autowire();
- Set scope = Collections.singleton("resource1.read");
- String accessToken = generateAccessToken(scope, CLIENT_EC_KEY);
- String dPoPProof = generateDPoPProof(HttpMethod.GET.name(), "http://localhost/resource1", accessToken);
- // @formatter:off
- this.mvc.perform(get("/resource1")
- .header(HttpHeaders.AUTHORIZATION, "DPoP " + accessToken)
- .header("DPoP", dPoPProof))
- .andExpect(status().isOk())
- .andExpect(content().string("resource1"));
- // @formatter:on
- verify(SecurityConfigWithCustomSuccessHandler.successHandler).onAuthenticationSuccess(any(), any(), any());
- }
-
private static String generateAccessToken(Set scope, JWK jwk) {
Map jktClaim = null;
if (jwk != null) {
@@ -268,11 +245,10 @@ public class DPoPAuthenticationConfigurerTests {
.requestMatchers("/resource2").hasAnyAuthority("SCOPE_resource2.read", "SCOPE_resource2.write")
.anyRequest().authenticated()
)
- .oauth2ResourceServer((oauth2ResourceServer) ->
- oauth2ResourceServer
- .jwt(Customizer.withDefaults())
- .dpop(Customizer.withDefaults())
- );
+ .oauth2ResourceServer((oauth2) -> oauth2
+ .jwt(Customizer.withDefaults())
+ .dPoP(Customizer.withDefaults()));
+
// @formatter:on
return http.build();
}
@@ -284,48 +260,6 @@ public class DPoPAuthenticationConfigurerTests {
}
- @Configuration
- @EnableWebSecurity
- @EnableWebMvc
- static class SecurityConfigWithCustomSuccessHandler {
-
- static final CustomSuccessHandler successHandler = spy(CustomSuccessHandler.class);
-
- @Bean
- SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
- // @formatter:off
- http
- .authorizeHttpRequests((authorize) ->
- authorize
- .requestMatchers("/resource1").hasAnyAuthority("SCOPE_resource1.read", "SCOPE_resource1.write")
- .requestMatchers("/resource2").hasAnyAuthority("SCOPE_resource2.read", "SCOPE_resource2.write")
- .anyRequest().authenticated()
- )
- .oauth2ResourceServer((oauth2ResourceServer) ->
- oauth2ResourceServer
- .jwt(Customizer.withDefaults())
- .dpop((dpop) -> dpop.successHandler(successHandler))
- );
- // @formatter:on
- return http.build();
- }
-
- @Bean
- NimbusJwtDecoder jwtDecoder() {
- return NimbusJwtDecoder.withPublicKey(PROVIDER_RSA_PUBLIC_KEY).build();
- }
-
- static class CustomSuccessHandler implements AuthenticationSuccessHandler {
-
- @Override
- public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
- Authentication authentication) {
- }
-
- }
-
- }
-
@RestController
static class ResourceEndpoints {
diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDslTests.kt
index 441120450d..013c6279eb 100644
--- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDslTests.kt
+++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/DPoPDslTests.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2025 the original author or authors.
+ * Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@ import java.time.temporal.ChronoUnit
import java.util.*
/**
- * Tests for [DPoPDsl]
+ * Integration tests for OAuth 2.0 Demonstrating Proof of Possession (DPoP) support [DPoPDsl].
*
* @author Max Batischev
*/
@@ -202,7 +202,7 @@ class DPoPDslTests {
}
oauth2ResourceServer {
jwt { }
- dpop { }
+ dPoP { }
}
}
return http.build()
diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPAuthenticationEntryPoint.java
index 81d5c617a2..9b8bcb6c91 100644
--- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPAuthenticationEntryPoint.java
+++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPAuthenticationEntryPoint.java
@@ -33,6 +33,16 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.util.StringUtils;
+/**
+ * An {@link AuthenticationEntryPoint} implementation used to commence authentication for
+ * DPoP-protected resource requests.
+ *
+ * @author Joe Grandja
+ * @author Max Batischev
+ * @since 7.1
+ * @see RFC 9449 Section 7.1. The DPoP Authentication Scheme
+ */
public final class DPoPAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPRequestMatcher.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPRequestMatcher.java
deleted file mode 100644
index f67d631d6f..0000000000
--- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DPoPRequestMatcher.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2004-present the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.oauth2.server.resource.web;
-
-import jakarta.servlet.http.HttpServletRequest;
-
-import org.springframework.http.HttpHeaders;
-import org.springframework.security.oauth2.core.OAuth2AccessToken;
-import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.springframework.util.StringUtils;
-
-public final class DPoPRequestMatcher implements RequestMatcher {
-
- @Override
- public boolean matches(HttpServletRequest request) {
- String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
- if (!StringUtils.hasText(authorization)) {
- return false;
- }
- return StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue());
- }
-
-}
diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/DPoPAuthenticationConverter.java
similarity index 85%
rename from oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationConverter.java
rename to oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/DPoPAuthenticationConverter.java
index 6727710414..81aebc010b 100644
--- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationConverter.java
+++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/DPoPAuthenticationConverter.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.security.oauth2.server.resource.authentication;
+package org.springframework.security.oauth2.server.resource.web.authentication;
import java.util.Collections;
import java.util.List;
@@ -30,10 +30,22 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.server.resource.authentication.DPoPAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
+/**
+ * Attempts to extract a DPoP-bound access token from {@link HttpServletRequest} and then
+ * converts it to a {@link DPoPAuthenticationToken} used for authenticating the
+ * DPoP-protected resource request.
+ *
+ * @author Joe Grandja
+ * @author Max Batischev
+ * @since 7.1
+ * @see AuthenticationConverter
+ * @see DPoPAuthenticationToken
+ */
public final class DPoPAuthenticationConverter implements AuthenticationConverter {
private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^DPoP (?[a-zA-Z0-9-._~+/]+=*)$",