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-._~+/]+=*)$",