1
0
mirror of synced 2026-05-22 13:23:17 +00:00

Polish gh-17202

This commit is contained in:
Joe Grandja
2026-04-08 11:06:46 -04:00
parent fc6a4c8220
commit f8359ef619
11 changed files with 251 additions and 363 deletions
@@ -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 <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC 9449
* OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
*/
public final class DPoPAuthenticationConfigurer<B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<DPoPAuthenticationConfigurer<B>, 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<B> resourceServerConfigurer = http
.getConfigurer(OAuth2ResourceServerConfigurer.class);
final AuthenticationManagerResolver<HttpServletRequest> 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<B> 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<B> 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<B> 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<B> 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;
}
}
@@ -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 &lt;ch4mp@c4-soft.com&gt;
* @author Joe Grandja
* @author Max Batischev
* @since 5.1
* @see BearerTokenAuthenticationFilter
@@ -169,8 +185,6 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
private final ApplicationContext context;
private DPoPAuthenticationConfigurer<H> dPoPAuthenticationConfigurer;
private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
private AuthenticationConverter authenticationConverter;
@@ -179,6 +193,8 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
private OpaqueTokenConfigurer opaqueTokenConfigurer;
private DPoPConfigurer dPoPConfigurer;
private final ProtectedResourceMetadataConfigurer protectedResourceMetadataConfigurer = new ProtectedResourceMetadataConfigurer();
private AccessDeniedHandler accessDeniedHandler = new DelegatingAccessDeniedHandler(
@@ -259,6 +275,21 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this;
}
/**
* Enables DPoP-bound access token support.
* @param dPoPCustomizer the {@link Customizer} to provide more options for the
* {@link DPoPConfigurer}
* @return the {@link OAuth2ResourceServerConfigurer} for further customizations
* @since 7.1
*/
public OAuth2ResourceServerConfigurer<H> dPoP(Customizer<DPoPConfigurer> 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<H extends HttpSecurityBuilder<
return this;
}
/**
* Enables DPoP support.
* @param dpopAuthenticatioCustomizer the {@link Customizer} to provide more options
* for the {@link DPoPAuthenticationConfigurer}
* @return the {@link OAuth2ResourceServerConfigurer} for further customizations
* @since 7.0
*/
public OAuth2ResourceServerConfigurer<H> dpop(
Customizer<DPoPAuthenticationConfigurer<H>> 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 OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
filter = postProcess(filter);
http.addFilter(filter);
if (dPoPAuthenticationAvailable && this.dPoPAuthenticationConfigurer != null) {
this.dPoPAuthenticationConfigurer.configure(http);
if (dPoPAuthenticationAvailable && this.dPoPConfigurer != null) {
this.dPoPConfigurer.configure(http);
}
OAuth2ProtectedResourceMetadataFilter protectedResourceMetadataFilter = new OAuth2ProtectedResourceMetadataFilter();
@@ -611,6 +626,153 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
}
/**
* A configurer for OAuth 2.0 Demonstrating Proof of Possession (DPoP) support.
*
* @author Joe Grandja
* @author Max Batischev
* @since 7.1
* @see AuthenticationFilter
* @see DPoPAuthenticationConverter
* @see DPoPAuthenticationEntryPoint
* @see DPoPAuthenticationProvider
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc9449">RFC
* 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)</a>
*/
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<HttpServletRequest> 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<OAuth2ProtectedResourceMetadata.Builder> protectedResourceMetadataCustomizer;
@@ -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<HttpSecurity>.JwtConfigurer) -> Unit)? = null
private var opaqueToken: ((OAuth2ResourceServerConfigurer<HttpSecurity>.OpaqueTokenConfigurer) -> Unit)? = null
private var dpop: ((DPoPAuthenticationConfigurer<HttpSecurity>) -> Unit)? = null
private var dPoP: ((OAuth2ResourceServerConfigurer<HttpSecurity>.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<HttpSecurity>) -> 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) }
}
}
}
@@ -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<HttpSecurity>) -> 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<HttpSecurity>.DPoPConfigurer) -> Unit {
return { dPoP ->
requestMatcher?.also { dPoP.requestMatcher(requestMatcher) }
authenticationConverter?.also { dPoP.authenticationConverter(authenticationConverter) }
authenticationSuccessHandler?.also { dPoP.authenticationSuccessHandler(authenticationSuccessHandler) }
authenticationFailureHandler?.also { dPoP.authenticationFailureHandler(authenticationFailureHandler) }
}
}
}
@@ -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}
@@ -2074,26 +2074,6 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="dpop">
<xs:annotation>
<xs:documentation>Configuration DpoP
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attributeGroup ref="security:dpop.attlist"/>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="dpop.attlist">
<xs:attribute name="dpop-request-matcher-ref" type="xs:token">
<xs:annotation>
<xs:documentation>DPoP Request Matcher
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="dpop-authentication-converter-ref" type="xs:token"/>
<xs:attribute name="dpop-success-handler-ref" type="xs:token"/>
<xs:attribute name="dpop-failure-handler-ref" type="xs:token"/>
</xs:attributeGroup>
<xs:attributeGroup name="saml2-login.attlist">
<xs:attribute name="relying-party-registration-repository-ref" type="xs:token">
@@ -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<String> 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<String> scope, JWK jwk) {
Map<String, Object> 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 {
@@ -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()