From 227a2cc0c72b06f718fd62b6d1a54a60c613dc9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 03:51:03 +0000 Subject: [PATCH 01/10] Bump com.webauthn4j:webauthn4j-core Bumps [com.webauthn4j:webauthn4j-core](https://github.com/webauthn4j/webauthn4j) from 0.29.2.RELEASE to 0.29.3.RELEASE. - [Release notes](https://github.com/webauthn4j/webauthn4j/releases) - [Changelog](https://github.com/webauthn4j/webauthn4j/blob/master/github-release-notes-generator.yml) - [Commits](https://github.com/webauthn4j/webauthn4j/compare/0.29.2.RELEASE...0.29.3.RELEASE) --- updated-dependencies: - dependency-name: com.webauthn4j:webauthn4j-core dependency-version: 0.29.3.RELEASE dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a16532177..efc1c81dbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -108,7 +108,7 @@ org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-inf org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969" org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1" -webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.29.2.RELEASE' +webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.29.3.RELEASE' [plugins] From b20cfceabbd1fe9c019cfb724272f563ed667a63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:48:10 +0000 Subject: [PATCH 02/10] Bump io-spring-javaformat from 0.0.45 to 0.0.46 Bumps `io-spring-javaformat` from 0.0.45 to 0.0.46. Updates `io.spring.javaformat:spring-javaformat-checkstyle` from 0.0.45 to 0.0.46 - [Release notes](https://github.com/spring-io/spring-javaformat/releases) - [Commits](https://github.com/spring-io/spring-javaformat/compare/v0.0.45...v0.0.46) Updates `io.spring.javaformat:spring-javaformat-gradle-plugin` from 0.0.45 to 0.0.46 - [Release notes](https://github.com/spring-io/spring-javaformat/releases) - [Commits](https://github.com/spring-io/spring-javaformat/compare/v0.0.45...v0.0.46) --- updated-dependencies: - dependency-name: io.spring.javaformat:spring-javaformat-checkstyle dependency-version: 0.0.46 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.spring.javaformat:spring-javaformat-gradle-plugin dependency-version: 0.0.46 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6a16532177..f5379aff42 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] com-squareup-okhttp3 = "3.14.9" io-rsocket = "1.1.5" -io-spring-javaformat = "0.0.45" +io-spring-javaformat = "0.0.46" io-spring-nohttp = "0.0.11" jakarta-websocket = "2.2.0" org-apache-directory-server = "1.5.5" From 33ae1711a76acc0eafa93ccf40d1ad7c09a35830 Mon Sep 17 00:00:00 2001 From: Evgeniy Cheban Date: Sat, 31 May 2025 05:05:15 +0300 Subject: [PATCH 03/10] Set Precedence Order for Spring MVC TargetVisitor Closes gh-17185 Signed-off-by: Evgeniy Cheban --- .../AuthorizationProxyWebConfiguration.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyWebConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyWebConfiguration.java index 4af062ef96..d6cdea7279 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyWebConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyWebConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; +import org.springframework.core.Ordered; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; @@ -37,7 +38,9 @@ class AuthorizationProxyWebConfiguration { return new WebTargetVisitor(); } - static class WebTargetVisitor implements AuthorizationAdvisorProxyFactory.TargetVisitor { + static class WebTargetVisitor implements AuthorizationAdvisorProxyFactory.TargetVisitor, Ordered { + + private static final int DEFAULT_ORDER = 100; @Override public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) { @@ -60,6 +63,11 @@ class AuthorizationProxyWebConfiguration { return null; } + @Override + public int getOrder() { + return DEFAULT_ORDER; + } + } } From 4967f3feee0b0f5906132eb3467fff3860688463 Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Fri, 11 Apr 2025 15:26:00 +0300 Subject: [PATCH 04/10] Add Support BearerTokenAuthenticationConverter Closes gh-14750 Signed-off-by: Max Batischev --- .../OAuth2ResourceServerConfigurer.java | 97 ++++++++-- ...th2ResourceServerBeanDefinitionParser.java | 76 +++++++- .../security/config/spring-security-6.5.rnc | 3 + .../security/config/spring-security-6.5.xsd | 6 + .../OAuth2ResourceServerConfigurerTests.java | 83 +++++++- ...sourceServerBeanDefinitionParserTests.java | 41 ++-- ...ionParserTests-AuthenticationConverter.xml | 32 ++++ ...icationConverterAndBearerTokenResolver.xml | 32 ++++ ...arserTests-MockAuthenticationConverter.xml | 27 +++ .../servlet/appendix/namespace/http.adoc | 4 + .../resource/web/BearerTokenResolver.java | 5 +- .../BearerTokenAuthenticationConverter.java | 181 ++++++++++++++++++ .../BearerTokenAuthenticationFilter.java | 35 +++- ...arerTokenAuthenticationConverterTests.java | 148 ++++++++++++++ .../BearerTokenAuthenticationFilterTests.java | 10 + 15 files changed, 722 insertions(+), 58 deletions(-) create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverter.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverterAndBearerTokenResolver.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockAuthenticationConverter.xml create mode 100644 oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java 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 e9a425d46d..8d44dfdcb5 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 @@ -23,6 +23,8 @@ import java.util.Map; import java.util.function.Supplier; import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; @@ -37,10 +39,12 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; 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.authentication.BearerTokenAuthenticationToken; 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; @@ -49,13 +53,14 @@ 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.DefaultBearerTokenResolver; 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.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.csrf.CsrfException; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; @@ -64,6 +69,7 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; 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.StringUtils; import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy; @@ -156,7 +162,7 @@ public final class OAuth2ResourceServerConfigurer authenticationManagerResolver; - private BearerTokenResolver bearerTokenResolver; + private AuthenticationConverter authenticationConverter; private JwtConfigurer jwtConfigurer; @@ -194,9 +200,25 @@ public final class OAuth2ResourceServerConfigurer bearerTokenResolver(BearerTokenResolver bearerTokenResolver) { Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); - this.bearerTokenResolver = bearerTokenResolver; + this.authenticationConverter = new BearerTokenResolverAuthenticationConverterAdapter(bearerTokenResolver); + return this; + } + + /** + * Sets the {@link AuthenticationConverter} to use. + * @param authenticationConverter the authentication converter + * @return the {@link OAuth2ResourceServerConfigurer} for further configuration + * @since 6.5 + */ + public OAuth2ResourceServerConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; return this; } @@ -271,8 +293,6 @@ public final class OAuth2ResourceServerConfigurer 0) { - this.bearerTokenResolver = this.context.getBean(BearerTokenResolver.class); - } - else { - this.bearerTokenResolver = new DefaultBearerTokenResolver(); - } + AuthenticationConverter getAuthenticationConverter() { + if (this.authenticationConverter != null) { + return this.authenticationConverter; } - return this.bearerTokenResolver; + if (this.context.getBeanNamesForType(AuthenticationConverter.class).length > 0) { + this.authenticationConverter = this.context.getBean(AuthenticationConverter.class); + } + else if (this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0) { + BearerTokenResolver bearerTokenResolver = this.context.getBean(BearerTokenResolver.class); + this.authenticationConverter = new BearerTokenResolverAuthenticationConverterAdapter(bearerTokenResolver); + } + else { + this.authenticationConverter = new BearerTokenAuthenticationConverter(); + } + return this.authenticationConverter; + } + + BearerTokenResolver getBearerTokenResolver() { + AuthenticationConverter authenticationConverter = getAuthenticationConverter(); + if (authenticationConverter instanceof BearerTokenResolverAuthenticationConverterAdapter bearer) { + return bearer.bearerTokenResolver; + } + return null; } public class JwtConfigurer { @@ -560,21 +595,43 @@ public final class OAuth2ResourceServerConfigurer + + + Reference to a AuthenticationConverter + + + diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 6b263c7048..5c655f5afd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -127,12 +127,14 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthen import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; 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.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; @@ -759,13 +761,6 @@ public class OAuth2ResourceServerConfigurerTests { assertThat(oauth2.getBearerTokenResolver()).isEqualTo(resolver); } - @Test - public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed() { - ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext(); - OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); - assertThat(oauth2.getBearerTokenResolver()).isInstanceOf(DefaultBearerTokenResolver.class); - } - @Test public void requestWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception { this.spring.register(CustomAuthenticationDetailsSource.class, JwtDecoderConfig.class, BasicController.class) @@ -1415,6 +1410,47 @@ public class OAuth2ResourceServerConfigurerTests { verify(authenticationConverter).convert(any(), any()); } + @Test + public void getAuthenticationConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() { + AuthenticationConverter converter = mock(AuthenticationConverter.class); + AuthenticationConverter converterBean = mock(AuthenticationConverter.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("converterOne", AuthenticationConverter.class, () -> converterBean); + context.registerBean("converterTwo", AuthenticationConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + oauth2.authenticationConverter(converter); + assertThat(oauth2.getAuthenticationConverter()).isEqualTo(converter); + } + + @Test + public void getAuthenticationConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() { + AuthenticationConverter converter = mock(AuthenticationConverter.class); + AuthenticationConverter converterBean = mock(AuthenticationConverter.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean(AuthenticationConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + oauth2.authenticationConverter(converter); + assertThat(oauth2.getAuthenticationConverter()).isEqualTo(converter); + } + + @Test + public void getAuthenticationConverterWhenDuplicateConverterBeansThenWiringException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy( + () -> this.spring.register(MultipleAuthenticationConverterBeansConfig.class, JwtDecoderConfig.class) + .autowire()) + .withRootCauseInstanceOf(NoUniqueBeanDefinitionException.class); + } + + @Test + public void getAuthenticationConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() { + ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext(); + OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context); + assertThat(oauth2.getAuthenticationConverter()).isInstanceOf(BearerTokenAuthenticationConverter.class); + } + private static void registerMockBean(GenericApplicationContext context, String name, Class clazz) { context.registerBean(name, clazz, () -> mock(clazz)); } @@ -2516,6 +2552,39 @@ public class OAuth2ResourceServerConfigurerTests { } + @Configuration + @EnableWebSecurity + static class MultipleAuthenticationConverterBeansConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2ResourceServer() + .jwt(); + return http.build(); + // @formatter:on + } + + @Bean + AuthenticationConverter authenticationConverterOne() { + BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter(); + converter.setAllowUriQueryParameter(true); + return converter; + } + + @Bean + AuthenticationConverter authenticationConverterTwo() { + BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter(); + converter.setAllowUriQueryParameter(true); + return converter; + } + + } + @Configuration @EnableWebSecurity static class MultipleIssuersConfig { diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java index 6a01051ca4..5ad167eef8 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -25,7 +25,6 @@ import java.time.Instant; import java.time.ZoneId; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Properties; import java.util.stream.Collectors; @@ -50,13 +49,11 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.w3c.dom.Element; -import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.beans.factory.xml.XmlReaderContext; @@ -85,12 +82,14 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.TestJwts; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -462,6 +461,24 @@ public class OAuth2ResourceServerBeanDefinitionParserTests { verify(bearerTokenResolver).resolve(any(HttpServletRequest.class)); } + @Test + public void getWhenCustomAuthenticationConverterThenUses() throws Exception { + this.spring + .configLocations(xml("MockAuthenticationConverter"), xml("MockJwtDecoder"), xml("AuthenticationConverter")) + .autowire(); + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + given(decoder.decode("token")).willReturn(TestJwts.jwt().build()); + AuthenticationConverter authenticationConverter = this.spring.getContext() + .getBean(AuthenticationConverter.class); + given(authenticationConverter.convert(any(HttpServletRequest.class))) + .willReturn(new BearerTokenAuthenticationToken("token")); + + this.mvc.perform(get("/")).andExpect(status().isNotFound()); + + verify(decoder).decode("token"); + verify(authenticationConverter).convert(any(HttpServletRequest.class)); + } + @Test public void requestWhenBearerTokenResolverAllowsRequestBodyThenEitherHeaderOrRequestBodyIsAccepted() throws Exception { @@ -521,14 +538,6 @@ public class OAuth2ResourceServerBeanDefinitionParserTests { // @formatter:on } - @Test - public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed() { - OAuth2ResourceServerBeanDefinitionParser oauth2 = new OAuth2ResourceServerBeanDefinitionParser( - mock(BeanReference.class), mock(List.class), mock(Map.class), mock(Map.class), mock(List.class), - mock(BeanMetadataElement.class)); - assertThat(oauth2.getBearerTokenResolver(mock(Element.class))).isInstanceOf(RootBeanDefinition.class); - } - @Test public void requestWhenCustomJwtDecoderThenUsed() throws Exception { this.spring.configLocations(xml("MockJwtDecoder"), xml("Jwt")).autowire(); @@ -545,6 +554,12 @@ public class OAuth2ResourceServerBeanDefinitionParserTests { .isThrownBy(() -> this.spring.configLocations(xml("JwtDecoderAndJwkSetUri")).autowire()); } + @Test + public void configureWhenAuthenticationConverterAndJwkSetUriThenException() { + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy( + () -> this.spring.configLocations(xml("AuthenticationConverterAndBearerTokenResolver")).autowire()); + } + @Test public void requestWhenRealmNameConfiguredThenUsesOnUnauthenticated() throws Exception { this.spring.configLocations(xml("MockJwtDecoder"), xml("AuthenticationEntryPoint")).autowire(); diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverter.xml new file mode 100644 index 0000000000..04d3932f09 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverter.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverterAndBearerTokenResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverterAndBearerTokenResolver.xml new file mode 100644 index 0000000000..c0cb49bf65 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverterAndBearerTokenResolver.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockAuthenticationConverter.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockAuthenticationConverter.xml new file mode 100644 index 0000000000..397c4c59bf --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockAuthenticationConverter.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 2b434d4303..d5438e0f43 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -1272,6 +1272,10 @@ Reference to a `BearerTokenResolver` which will retrieve the bearer token from t * **entry-point-ref** Reference to a `AuthenticationEntryPoint` which will handle unauthorized requests +[[nsa-oauth2-resource-server-authentication-converter-ref]] +* **authentication-converter-ref** +Reference to a `AuthenticationConverter` which convert request to authentication + [[nsa-jwt]] == Represents an OAuth 2.0 Resource Server that will authorize JWTs diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java index 7abd174630..0fd023c5f6 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -29,7 +29,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; * @since 5.1 * @see RFC 6750 * Section 2: Authenticated Requests + * @deprecated Use + * {@link org.springframework.security.web.authentication.AuthenticationConverter} instead */ +@Deprecated @FunctionalInterface public interface BearerTokenResolver { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java new file mode 100644 index 0000000000..211a49bf21 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2025 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.authentication; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.oauth2.server.resource.BearerTokenErrors; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Implementation of {@link AuthenticationConverter}, that converts request to + * {@link BearerTokenAuthenticationToken} + * + * @author Max Batischev + * @since 6.5 + */ +public final class BearerTokenAuthenticationConverter implements AuthenticationConverter { + + private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + + private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-._~+/]+=*)$", + Pattern.CASE_INSENSITIVE); + + private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token"; + + private boolean allowFormEncodedBodyParameter = false; + + private boolean allowUriQueryParameter = false; + + private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; + + @Override + public Authentication convert(HttpServletRequest request) { + String token = resolveToken(request); + if (StringUtils.hasText(token)) { + BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token); + authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request)); + + return authenticationToken; + } + return null; + } + + private String resolveToken(HttpServletRequest request) { + final String authorizationHeaderToken = resolveFromAuthorizationHeader(request); + final String parameterToken = isParameterTokenSupportedForRequest(request) + ? resolveFromRequestParameters(request) : null; + if (authorizationHeaderToken != null) { + if (parameterToken != null) { + final BearerTokenError error = BearerTokenErrors + .invalidRequest("Found multiple bearer tokens in the request"); + throw new OAuth2AuthenticationException(error); + } + return authorizationHeaderToken; + } + if (parameterToken != null && isParameterTokenEnabledForRequest(request)) { + return parameterToken; + } + return null; + } + + private String resolveFromAuthorizationHeader(HttpServletRequest request) { + String authorization = request.getHeader(this.bearerTokenHeaderName); + if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) { + return null; + } + Matcher matcher = authorizationPattern.matcher(authorization); + if (!matcher.matches()) { + BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed"); + throw new OAuth2AuthenticationException(error); + } + return matcher.group("token"); + } + + private boolean isParameterTokenEnabledForRequest(HttpServletRequest request) { + return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request) + && !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request))); + } + + private static String resolveFromRequestParameters(HttpServletRequest request) { + String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME); + if (values == null || values.length == 0) { + return null; + } + if (values.length == 1) { + return values[0]; + } + BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request"); + throw new OAuth2AuthenticationException(error); + } + + private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) { + return isFormEncodedRequest(request) || isGetRequest(request); + } + + private boolean isGetRequest(HttpServletRequest request) { + return HttpMethod.GET.name().equals(request.getMethod()); + } + + private boolean isFormEncodedRequest(HttpServletRequest request) { + return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()); + } + + private static boolean hasAccessTokenInQueryString(HttpServletRequest request) { + return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME); + } + + /** + * Set if transport of access token using URI query parameter is supported. Defaults + * to {@code false}. + * + * The spec recommends against using this mechanism for sending bearer tokens, and + * even goes as far as stating that it was only included for completeness. + * @param allowUriQueryParameter if the URI query parameter is supported + */ + public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { + this.allowUriQueryParameter = allowUriQueryParameter; + } + + /** + * Set this value to configure what header is checked when resolving a Bearer Token. + * This value is defaulted to {@link HttpHeaders#AUTHORIZATION}. + * + * This allows other headers to be used as the Bearer Token source such as + * {@link HttpHeaders#PROXY_AUTHORIZATION} + * @param bearerTokenHeaderName the header to check when retrieving the Bearer Token. + */ + public void setBearerTokenHeaderName(String bearerTokenHeaderName) { + this.bearerTokenHeaderName = bearerTokenHeaderName; + } + + /** + * Set if transport of access token using form-encoded body parameter is supported. + * Defaults to {@code false}. + * @param allowFormEncodedBodyParameter if the form-encoded body parameter is + * supported + */ + public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) { + this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter; + } + + /** + * Set the {@link AuthenticationDetailsSource} to use. Defaults to + * {@link WebAuthenticationDetailsSource}. + * @param authenticationDetailsSource the {@code AuthenticationDetailsSource} to use + */ + public void setAuthenticationDetailsSource( + AuthenticationDetailsSource authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); + this.authenticationDetailsSource = authenticationDetailsSource; + } + +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java index 9cad61d0cb..5aa819f6be 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java @@ -44,6 +44,7 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthen import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; import org.springframework.security.web.AuthenticationEntryPoint; +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.WebAuthenticationDetailsSource; @@ -83,12 +84,12 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { private AuthenticationFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler( (request, response, exception) -> this.authenticationEntryPoint.commence(request, response, exception)); - private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); - private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository(); + private AuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter(); + /** * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) * @param authenticationManagerResolver @@ -121,24 +122,22 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String token; + Authentication authenticationRequest; try { - token = this.bearerTokenResolver.resolve(request); + authenticationRequest = this.authenticationConverter.convert(request); } catch (OAuth2AuthenticationException invalid) { this.logger.trace("Sending to authentication entry point since failed to resolve bearer token", invalid); this.authenticationEntryPoint.commence(request, response, invalid); return; } - if (token == null) { + + if (authenticationRequest == null) { this.logger.trace("Did not process request since did not find bearer token"); filterChain.doFilter(request, response); return; } - BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token); - authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); - try { AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request); Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest); @@ -194,7 +193,14 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { */ public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); - this.bearerTokenResolver = bearerTokenResolver; + this.authenticationConverter = (request) -> { + String token = bearerTokenResolver.resolve(request); + if (!StringUtils.hasText(token)) { + this.logger.trace("Did not process request since did not find bearer token"); + return null; + } + return new BearerTokenAuthenticationToken(token); + }; } /** @@ -243,4 +249,15 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { return StringUtils.hasText(jwkThumbprintClaim); } + /** + * Set the {@link AuthenticationConverter} to use. Defaults to + * {@link BearerTokenAuthenticationConverter}. + * @param authenticationConverter the {@code AuthenticationConverter} to use + * @since 6.5 + */ + public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + } + } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java new file mode 100644 index 0000000000..a5655a0c11 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2025 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.authentication; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link BearerTokenAuthenticationConverter} + * + * @author Max Batischev + */ +public class BearerTokenAuthenticationConverterTests { + + private static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token"; + + private static final String TEST_X_AUTH_TOKEN = "test-x-auth-token"; + + private static final String BEARER_TOKEN = "test_bearer_token"; + + private final BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter(); + + @Test + public void convertWhenAuthorizationHeaderIsPresentThenTokenIsConverted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BEARER_TOKEN); + + Authentication authentication = this.converter.convert(request); + + assertThat(authentication).isNotNull(); + } + + @Test + public void convertWhenQueryParameterIsPresentThenTokenIsConverted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod(HttpMethod.GET.name()); + request.addParameter("access_token", BEARER_TOKEN); + + this.converter.setAllowUriQueryParameter(true); + + Authentication authentication = this.converter.convert(request); + assertThat(authentication).isNotNull(); + } + + @Test + public void convertWhenAuthorizationHeaderNotIsPresentThenTokenIsNotConverted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + Authentication authentication = this.converter.convert(request); + + assertThat(authentication).isNull(); + } + + @Test + public void convertWhenAuthorizationHeaderIsPresentTogetherWithQueryParameterThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("access_token", BEARER_TOKEN); + request.setMethod(HttpMethod.GET.name()); + request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BEARER_TOKEN); + + assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.converter.convert(request)) + .withMessageContaining("Found multiple bearer tokens in the request"); + } + + @Test + public void convertWhenXAuthTokenHeaderIsPresentAndBearerTokenHeaderNameSetThenTokenIsConverted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(X_AUTH_TOKEN_HEADER, "Bearer " + TEST_X_AUTH_TOKEN); + + this.converter.setBearerTokenHeaderName(X_AUTH_TOKEN_HEADER); + + Authentication authentication = this.converter.convert(request); + assertThat(authentication).isNotNull(); + } + + @Test + public void convertWhenHeaderWithMissingTokenIsPresentThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer "); + + assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.converter.convert(request)) + .withMessageContaining(("Bearer token is malformed")); + } + + @Test + public void convertWhenHeaderWithInvalidCharactersIsPresentThenAuthenticationExceptionIsThrown() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer an\"invalid\"token"); + + assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.converter.convert(request)) + .withMessageContaining(("Bearer token is malformed")); + } + + @Test + @SuppressWarnings("unchecked") + public void convertWhenCustomAuthenticationDetailsSourceSetThenTokenIsConverted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BEARER_TOKEN); + AuthenticationDetailsSource authenticationDetailsSource = Mockito + .mock(AuthenticationDetailsSource.class); + this.converter.setAuthenticationDetailsSource(authenticationDetailsSource); + + Authentication authentication = this.converter.convert(request); + + verify(authenticationDetailsSource).buildDetails(any()); + assertThat(authentication).isNotNull(); + } + + @Test + public void convertWhenFormParameterIsPresentAndAllowFormEncodedBodyParameterThenConverted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod(HttpMethod.POST.name()); + request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + request.addParameter("access_token", BEARER_TOKEN); + this.converter.setAllowFormEncodedBodyParameter(true); + + assertThat(this.converter.convert(request)).isNotNull(); + } + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java index cc7477684f..465b5261c6 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java @@ -293,6 +293,16 @@ public class BearerTokenAuthenticationFilterTests { // @formatter:on } + @Test + public void setConverterWhenNullThenThrowsException() { + // @formatter:off + BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(this.authenticationManager); + assertThatIllegalArgumentException() + .isThrownBy(() -> filter.setAuthenticationConverter(null)) + .withMessageContaining("authenticationConverter cannot be null"); + // @formatter:on + } + @Test public void constructorWhenNullAuthenticationManagerThenThrowsException() { // @formatter:off From 30577bd291781d34b4550404872cfba657278edc Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Fri, 11 Apr 2025 15:36:23 +0300 Subject: [PATCH 05/10] Add Additional Tests To BearerTokenAuthenticationFilterTests Issue gh-14750 Signed-off-by: Max Batischev --- .../BearerTokenAuthenticationFilterTests.java | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java index 465b5261c6..b64a29f762 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java @@ -52,6 +52,7 @@ import org.springframework.security.oauth2.server.resource.authentication.Bearer import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; @@ -74,6 +75,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) public class BearerTokenAuthenticationFilterTests { + private static final String TEST_TOKEN = "token"; + @Mock AuthenticationEntryPoint authenticationEntryPoint; @@ -92,6 +95,9 @@ public class BearerTokenAuthenticationFilterTests { @Mock AuthenticationDetailsSource authenticationDetailsSource; + @Mock + AuthenticationConverter authenticationConverter; + MockHttpServletRequest request; MockHttpServletResponse response; @@ -321,6 +327,171 @@ public class BearerTokenAuthenticationFilterTests { // @formatter:on } + @Test + public void doFilterWhenBearerTokenPresentAndConverterSetThenAuthenticates() throws ServletException, IOException { + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + + filter.doFilter(this.request, this.response, this.filterChain); + + ArgumentCaptor captor = ArgumentCaptor + .forClass(BearerTokenAuthenticationToken.class); + verify(this.authenticationManager).authenticate(captor.capture()); + assertThat(captor.getValue().getPrincipal()).isEqualTo(TEST_TOKEN); + assertThat(this.request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME)) + .isNotNull(); + } + + @Test + public void doFilterWhenSecurityContextRepositoryAndConverterSetThenSaves() throws ServletException, IOException { + SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + TestingAuthenticationToken expectedAuthentication = new TestingAuthenticationToken("test", "password"); + given(this.authenticationManager.authenticate(any())).willReturn(expectedAuthentication); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + filter.setSecurityContextRepository(securityContextRepository); + + filter.doFilter(this.request, this.response, this.filterChain); + + ArgumentCaptor captor = ArgumentCaptor + .forClass(BearerTokenAuthenticationToken.class); + verify(this.authenticationManager).authenticate(captor.capture()); + assertThat(captor.getValue().getPrincipal()).isEqualTo(TEST_TOKEN); + ArgumentCaptor contextArg = ArgumentCaptor.forClass(SecurityContext.class); + verify(securityContextRepository).saveContext(contextArg.capture(), eq(this.request), eq(this.response)); + assertThat(contextArg.getValue().getAuthentication().getName()).isEqualTo(expectedAuthentication.getName()); + } + + @Test + public void doFilterWhenUsingAuthenticationManagerResolverAndConverterSetThenAuthenticates() throws Exception { + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManagerResolver)); + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + given(this.authenticationManagerResolver.resolve(any())).willReturn(this.authenticationManager); + + filter.doFilter(this.request, this.response, this.filterChain); + + ArgumentCaptor captor = ArgumentCaptor + .forClass(BearerTokenAuthenticationToken.class); + verify(this.authenticationManager).authenticate(captor.capture()); + assertThat(captor.getValue().getPrincipal()).isEqualTo(TEST_TOKEN); + assertThat(this.request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME)) + .isNotNull(); + } + + @Test + public void doFilterWhenNoBearerTokenPresentAndConverterSetThenDoesNotAuthenticate() + throws ServletException, IOException { + given(this.authenticationConverter.convert(this.request)).willReturn(null); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + + filter.doFilter(this.request, this.response, this.filterChain); + + verifyNoMoreInteractions(this.authenticationManager); + } + + @Test + public void doFilterWhenMalformedBearerTokenAndConverterSetThenPropagatesError() + throws ServletException, IOException { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST, + "description", "uri"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); + given(this.authenticationConverter.convert(this.request)).willThrow(exception); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + filter.doFilter(this.request, this.response, this.filterChain); + + verifyNoMoreInteractions(this.authenticationManager); + verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); + } + + @Test + public void doFilterWhenAuthenticationFailsWithDefaultHandlerAndConverterSetThenPropagatesError() + throws ServletException, IOException { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, + "description", "uri"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + given(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class))).willThrow(exception); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + + filter.doFilter(this.request, this.response, this.filterChain); + + verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); + } + + @Test + public void doFilterWhenAuthenticationFailsWithCustomHandlerAndConverterSetThenPropagatesError() + throws ServletException, IOException { + BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, + "description", "uri"); + OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + given(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class))).willThrow(exception); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + filter.setAuthenticationFailureHandler(this.authenticationFailureHandler); + + filter.doFilter(this.request, this.response, this.filterChain); + + verify(this.authenticationFailureHandler).onAuthenticationFailure(this.request, this.response, exception); + } + + @Test + public void doFilterWhenConverterSetAndAuthenticationServiceExceptionThenRethrows() { + AuthenticationServiceException exception = new AuthenticationServiceException("message"); + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + given(this.authenticationManager.authenticate(any())).willThrow(exception); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + + assertThatExceptionOfType(AuthenticationServiceException.class) + .isThrownBy(() -> filter.doFilter(this.request, this.response, this.filterChain)); + } + + @Test + public void doFilterWhenConverterSetAndCustomEntryPointAndAuthenticationErrorThenUses() + throws ServletException, IOException { + AuthenticationException exception = new InvalidBearerTokenException("message"); + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + given(this.authenticationManager.authenticate(any())).willThrow(exception); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + AuthenticationEntryPoint entrypoint = mock(AuthenticationEntryPoint.class); + filter.setAuthenticationEntryPoint(entrypoint); + + filter.doFilter(this.request, this.response, this.filterChain); + + verify(entrypoint).commence(any(), any(), any(InvalidBearerTokenException.class)); + } + + @Test + public void doFilterWhenConverterSetCustomSecurityContextHolderStrategyThenUses() + throws ServletException, IOException { + given(this.authenticationConverter.convert(this.request)) + .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); + BearerTokenAuthenticationFilter filter = addMocksWithConverter( + new BearerTokenAuthenticationFilter(this.authenticationManager)); + SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); + given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl()); + filter.setSecurityContextHolderStrategy(strategy); + + filter.doFilter(this.request, this.response, this.filterChain); + + verify(strategy).setContext(any()); + } + private BearerTokenAuthenticationFilter addMocks(BearerTokenAuthenticationFilter filter) { filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); filter.setBearerTokenResolver(this.bearerTokenResolver); @@ -335,4 +506,10 @@ public class BearerTokenAuthenticationFilterTests { verifyNoMoreInteractions(this.authenticationManager); } + private BearerTokenAuthenticationFilter addMocksWithConverter(BearerTokenAuthenticationFilter filter) { + filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); + filter.setAuthenticationConverter(this.authenticationConverter); + return filter; + } + } From eaab42a73c988523120441678d1f74d256465c9a Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:35:31 -0600 Subject: [PATCH 06/10] Polish BearerTokenAuthenticationConverter Support - Moved to BearerTokenAuthenticationFilter constructor to align with AuthenticationFilter - Undeprecated BearerTokenResolver to reduce number of migration scenarios - Updated to 7.0 schema - Added migration docs Issue gh-14750 --- .../OAuth2ResourceServerConfigurer.java | 37 ++-- ...th2ResourceServerBeanDefinitionParser.java | 12 +- .../security/config/spring-security-6.5.rnc | 3 - .../security/config/spring-security-6.5.xsd | 6 - .../security/config/spring-security-7.0.rnc | 3 + .../security/config/spring-security-7.0.xsd | 6 + .../OAuth2ResourceServerConfigurerTests.java | 16 +- .../ROOT/pages/migration/servlet/oauth2.adoc | 55 +++++ .../servlet/appendix/namespace/http.adoc | 6 +- .../resource/web/BearerTokenResolver.java | 5 +- .../BearerTokenAuthenticationConverter.java | 127 +---------- .../BearerTokenAuthenticationFilter.java | 92 +++++--- ...arerTokenAuthenticationConverterTests.java | 14 +- .../BearerTokenAuthenticationFilterTests.java | 197 ++---------------- 14 files changed, 203 insertions(+), 376 deletions(-) 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 8d44dfdcb5..c4b976bc99 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 @@ -23,8 +23,6 @@ import java.util.Map; import java.util.function.Supplier; import jakarta.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; @@ -44,7 +42,6 @@ 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.authentication.BearerTokenAuthenticationToken; 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; @@ -69,7 +66,6 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher; 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.StringUtils; import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy; @@ -200,13 +196,9 @@ public final class OAuth2ResourceServerConfigurer bearerTokenResolver(BearerTokenResolver bearerTokenResolver) { Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); - this.authenticationConverter = new BearerTokenResolverAuthenticationConverterAdapter(bearerTokenResolver); + this.authenticationConverter = new BearerTokenResolverHoldingAuthenticationConverter(bearerTokenResolver); return this; } @@ -214,7 +206,7 @@ public final class OAuth2ResourceServerConfigurer authenticationConverter(AuthenticationConverter authenticationConverter) { Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); @@ -299,10 +291,9 @@ public final class OAuth2ResourceServerConfigurer authenticationManager; } - BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(resolver); AuthenticationConverter converter = getAuthenticationConverter(); this.requestMatcher.setAuthenticationConverter(converter); - filter.setAuthenticationConverter(converter); + BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(resolver, converter); filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); filter = postProcess(filter); @@ -394,7 +385,7 @@ public final class OAuth2ResourceServerConfigurer 0) { BearerTokenResolver bearerTokenResolver = this.context.getBean(BearerTokenResolver.class); - this.authenticationConverter = new BearerTokenResolverAuthenticationConverterAdapter(bearerTokenResolver); + this.authenticationConverter = new BearerTokenResolverHoldingAuthenticationConverter(bearerTokenResolver); } else { this.authenticationConverter = new BearerTokenAuthenticationConverter(); @@ -404,7 +395,7 @@ public final class OAuth2ResourceServerConfigurer - - - Reference to a AuthenticationConverter - - - 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 15d15b191b..bbf8622dfe 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 @@ -650,6 +650,9 @@ oauth2-resource-server.attlist &= oauth2-resource-server.attlist &= ## Reference to a AuthenticationEntryPoint attribute entry-point-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a AuthenticationConverter + attribute authentication-converter-ref {xsd:token}? jwt = ## Configures JWT authentication 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 34556b5549..2e3d6cf275 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 @@ -1999,6 +1999,12 @@ + + + Reference to a AuthenticationConverter + + + diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 5c655f5afd..23dde67586 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -2571,16 +2571,20 @@ public class OAuth2ResourceServerConfigurerTests { @Bean AuthenticationConverter authenticationConverterOne() { - BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter(); - converter.setAllowUriQueryParameter(true); - return converter; + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowUriQueryParameter(true); + BearerTokenAuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter(); + authenticationConverter.setBearerTokenResolver(resolver); + return authenticationConverter; } @Bean AuthenticationConverter authenticationConverterTwo() { - BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter(); - converter.setAllowUriQueryParameter(true); - return converter; + DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + resolver.setAllowUriQueryParameter(true); + BearerTokenAuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter(); + authenticationConverter.setBearerTokenResolver(resolver); + return authenticationConverter; } } diff --git a/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc b/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc index 6cdb9043dd..293abadddb 100644 --- a/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc +++ b/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc @@ -115,3 +115,58 @@ fun authenticationConverter(val registrations: RelyingPartyRegistrationRepositor ====== If you must continue using `Saml2AuthenticationTokenConverter`, `OpenSaml4AuthenticationTokenConverter`, or `OpenSaml5AuthenticationTokenConverter` to process GET requests, you can call `setShouldConvertGetRequests` to `true.` + +== Provide an AuthenticationConverter to BearerTokenAuthenticationFilter + +In Spring Security 7, `BearerTokenAuthenticationFilter#setBearerTokenResolver` and `#setAuthenticaionDetailsSource` are deprecated in favor of configuring those on `BearerTokenAuthenticationConverter`. + +The `oauth2ResourceServer` DSL addresses most use cases and you need to nothing. + +If you are setting a `BearerTokenResolver` or `AuthenticationDetailsSource` directly on `BearerTokenAuthenticationFilter` similar to the following: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(authenticationManager); +filter.setBearerTokenResolver(myBearerTokenResolver); +filter.setAuthenticationDetailsSource(myAuthenticationDetailsSource); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val filter = BearerTokenAuthenticationFilter(authenticationManager) +filter.setBearerTokenResolver(myBearerTokenResolver) +filter.setAuthenticationDetailsSource(myAuthenticationDetailsSource) +---- +====== + +you are encouraged to use `BearerTokenAuthenticationConverter` to specify both: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +BearerTokenAuthenticationConverter authenticationConverter = + new BearerTokenAuthenticationConverter(); +authenticationConverter.setBearerTokenResolver(myBearerTokenResolver); +authenticationConverter.setAuthenticationDetailsSource(myAuthenticationDetailsSource); +BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(authenticationManager, authenicationConverter); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val authenticationConverter = BearerTokenAuthenticationConverter() +authenticationConverter.setBearerTokenResolver(myBearerTokenResolver) +authenticationConverter.setAuthenticationDetailsSource(myAuthenticationDetailsSource) +val filter = BearerTokenAuthenticationFilter(authenticationManager, authenticationConverter) +---- +====== diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index d5438e0f43..8979d5ad29 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -1266,7 +1266,8 @@ Reference to an `AuthenticationManagerResolver` which will resolve the `Authenti [[nsa-oauth2-resource-server-bearer-token-resolver-ref]] * **bearer-token-resolver-ref** -Reference to a `BearerTokenResolver` which will retrieve the bearer token from the request +Reference to a `BearerTokenResolver` which will retrieve the bearer token from the request. +This cannot be used in conjunction with `authentication-converter-ref` [[nsa-oauth2-resource-server-entry-point-ref]] * **entry-point-ref** @@ -1274,7 +1275,8 @@ Reference to a `AuthenticationEntryPoint` which will handle unauthorized request [[nsa-oauth2-resource-server-authentication-converter-ref]] * **authentication-converter-ref** -Reference to a `AuthenticationConverter` which convert request to authentication +Reference to a `AuthenticationConverter` which convert request to authentication. +This cannot be used in conjunction with `bearer-token-resolver-ref` [[nsa-jwt]] == diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java index 0fd023c5f6..7abd174630 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2025 the original author or authors. + * Copyright 2002-2020 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. @@ -29,10 +29,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; * @since 5.1 * @see RFC 6750 * Section 2: Authenticated Requests - * @deprecated Use - * {@link org.springframework.security.web.authentication.AuthenticationConverter} instead */ -@Deprecated @FunctionalInterface public interface BearerTokenResolver { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java index 211a49bf21..9f7e91a40a 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java @@ -16,20 +16,13 @@ package org.springframework.security.oauth2.server.resource.web.authentication; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import jakarta.servlet.http.HttpServletRequest; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.server.resource.BearerTokenError; -import org.springframework.security.oauth2.server.resource.BearerTokenErrors; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.util.Assert; @@ -40,131 +33,29 @@ import org.springframework.util.StringUtils; * {@link BearerTokenAuthenticationToken} * * @author Max Batischev - * @since 6.5 + * @author Josh Cummings + * @since 7.0 */ public final class BearerTokenAuthenticationConverter implements AuthenticationConverter { private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); - private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?[a-zA-Z0-9-._~+/]+=*)$", - Pattern.CASE_INSENSITIVE); - - private static final String ACCESS_TOKEN_PARAMETER_NAME = "access_token"; - - private boolean allowFormEncodedBodyParameter = false; - - private boolean allowUriQueryParameter = false; - - private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; + private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); @Override public Authentication convert(HttpServletRequest request) { - String token = resolveToken(request); + String token = this.bearerTokenResolver.resolve(request); if (StringUtils.hasText(token)) { BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token); authenticationToken.setDetails(this.authenticationDetailsSource.buildDetails(request)); - return authenticationToken; } return null; } - private String resolveToken(HttpServletRequest request) { - final String authorizationHeaderToken = resolveFromAuthorizationHeader(request); - final String parameterToken = isParameterTokenSupportedForRequest(request) - ? resolveFromRequestParameters(request) : null; - if (authorizationHeaderToken != null) { - if (parameterToken != null) { - final BearerTokenError error = BearerTokenErrors - .invalidRequest("Found multiple bearer tokens in the request"); - throw new OAuth2AuthenticationException(error); - } - return authorizationHeaderToken; - } - if (parameterToken != null && isParameterTokenEnabledForRequest(request)) { - return parameterToken; - } - return null; - } - - private String resolveFromAuthorizationHeader(HttpServletRequest request) { - String authorization = request.getHeader(this.bearerTokenHeaderName); - if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) { - return null; - } - Matcher matcher = authorizationPattern.matcher(authorization); - if (!matcher.matches()) { - BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed"); - throw new OAuth2AuthenticationException(error); - } - return matcher.group("token"); - } - - private boolean isParameterTokenEnabledForRequest(HttpServletRequest request) { - return ((this.allowFormEncodedBodyParameter && isFormEncodedRequest(request) && !isGetRequest(request) - && !hasAccessTokenInQueryString(request)) || (this.allowUriQueryParameter && isGetRequest(request))); - } - - private static String resolveFromRequestParameters(HttpServletRequest request) { - String[] values = request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME); - if (values == null || values.length == 0) { - return null; - } - if (values.length == 1) { - return values[0]; - } - BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request"); - throw new OAuth2AuthenticationException(error); - } - - private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) { - return isFormEncodedRequest(request) || isGetRequest(request); - } - - private boolean isGetRequest(HttpServletRequest request) { - return HttpMethod.GET.name().equals(request.getMethod()); - } - - private boolean isFormEncodedRequest(HttpServletRequest request) { - return MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()); - } - - private static boolean hasAccessTokenInQueryString(HttpServletRequest request) { - return (request.getQueryString() != null) && request.getQueryString().contains(ACCESS_TOKEN_PARAMETER_NAME); - } - - /** - * Set if transport of access token using URI query parameter is supported. Defaults - * to {@code false}. - * - * The spec recommends against using this mechanism for sending bearer tokens, and - * even goes as far as stating that it was only included for completeness. - * @param allowUriQueryParameter if the URI query parameter is supported - */ - public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { - this.allowUriQueryParameter = allowUriQueryParameter; - } - - /** - * Set this value to configure what header is checked when resolving a Bearer Token. - * This value is defaulted to {@link HttpHeaders#AUTHORIZATION}. - * - * This allows other headers to be used as the Bearer Token source such as - * {@link HttpHeaders#PROXY_AUTHORIZATION} - * @param bearerTokenHeaderName the header to check when retrieving the Bearer Token. - */ - public void setBearerTokenHeaderName(String bearerTokenHeaderName) { - this.bearerTokenHeaderName = bearerTokenHeaderName; - } - - /** - * Set if transport of access token using form-encoded body parameter is supported. - * Defaults to {@code false}. - * @param allowFormEncodedBodyParameter if the form-encoded body parameter is - * supported - */ - public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) { - this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter; + public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { + Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); + this.bearerTokenResolver = bearerTokenResolver; } /** diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java index 5aa819f6be..6a5f5c4869 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java @@ -40,6 +40,7 @@ import org.springframework.security.oauth2.server.resource.BearerTokenErrors; import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; 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.DefaultBearerTokenResolver; @@ -76,6 +77,8 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationManagerResolver authenticationManagerResolver; + private final AuthenticationConverter authenticationConverter; + private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); @@ -84,20 +87,15 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { private AuthenticationFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler( (request, response, exception) -> this.authenticationEntryPoint.commence(request, response, exception)); - private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); - private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository(); - private AuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter(); - /** * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) * @param authenticationManagerResolver */ public BearerTokenAuthenticationFilter( AuthenticationManagerResolver authenticationManagerResolver) { - Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null"); - this.authenticationManagerResolver = authenticationManagerResolver; + this(authenticationManagerResolver, new BearerTokenAuthenticationConverter()); } /** @@ -105,8 +103,43 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { * @param authenticationManager */ public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) { + this(authenticationManager, new BearerTokenAuthenticationConverter()); + } + + /** + * Construct this filter using the provided parameters + * @param authenticationManager the {@link AuthenticationManager} to use + * @param authenticationConverter the {@link AuthenticationConverter} to use + * @since 7.0 + * @see JwtAuthenticationProvider + * @see OpaqueTokenAuthenticationProvider + * @see BearerTokenAuthenticationConverter + */ + public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager, + AuthenticationConverter authenticationConverter) { Assert.notNull(authenticationManager, "authenticationManager cannot be null"); - this.authenticationManagerResolver = (request) -> authenticationManager; + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationManagerResolver = (authentication) -> authenticationManager; + this.authenticationConverter = authenticationConverter; + } + + /** + * Construct this filter using the provided parameters + * @param authenticationManagerResolver the {@link AuthenticationManagerResolver} to + * use + * @param authenticationConverter the {@link AuthenticationConverter} to use + * @since 7.0 + * @see JwtAuthenticationProvider + * @see OpaqueTokenAuthenticationProvider + * @see BearerTokenAuthenticationConverter + */ + public BearerTokenAuthenticationFilter( + AuthenticationManagerResolver authenticationManagerResolver, + AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null"); + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationManagerResolver = authenticationManagerResolver; + this.authenticationConverter = authenticationConverter; } /** @@ -190,17 +223,20 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { * Set the {@link BearerTokenResolver} to use. Defaults to * {@link DefaultBearerTokenResolver}. * @param bearerTokenResolver the {@code BearerTokenResolver} to use + * @deprecated Please provide an {@link AuthenticationConverter} in the constructor + * instead + * @see BearerTokenAuthenticationConverter */ + @Deprecated public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); - this.authenticationConverter = (request) -> { - String token = bearerTokenResolver.resolve(request); - if (!StringUtils.hasText(token)) { - this.logger.trace("Did not process request since did not find bearer token"); - return null; - } - return new BearerTokenAuthenticationToken(token); - }; + if (this.authenticationConverter instanceof BearerTokenAuthenticationConverter converter) { + converter.setBearerTokenResolver(bearerTokenResolver); + } + else { + throw new IllegalArgumentException( + "You cannot both specify an AuthenticationConverter and a BearerTokenResolver."); + } } /** @@ -227,13 +263,24 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { /** * Set the {@link AuthenticationDetailsSource} to use. Defaults to * {@link WebAuthenticationDetailsSource}. - * @param authenticationDetailsSource the {@code AuthenticationConverter} to use + * @param authenticationDetailsSource the {@code AuthenticationDetailsSource} to use * @since 5.5 + * @deprecated Please provide an {@link AuthenticationConverter} in the constructor + * and set the {@link AuthenticationDetailsSource} there instead. For example, you can + * use {@link BearerTokenAuthenticationConverter#setAuthenticationDetailsSource} + * @see BearerTokenAuthenticationConverter */ + @Deprecated public void setAuthenticationDetailsSource( AuthenticationDetailsSource authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); - this.authenticationDetailsSource = authenticationDetailsSource; + if (this.authenticationConverter instanceof BearerTokenAuthenticationConverter converter) { + converter.setAuthenticationDetailsSource(authenticationDetailsSource); + } + else { + throw new IllegalArgumentException( + "You cannot specify both an AuthenticationConverter and an AuthenticationDetailsSource"); + } } private static boolean isDPoPBoundAccessToken(Authentication authentication) { @@ -249,15 +296,4 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { return StringUtils.hasText(jwkThumbprintClaim); } - /** - * Set the {@link AuthenticationConverter} to use. Defaults to - * {@link BearerTokenAuthenticationConverter}. - * @param authenticationConverter the {@code AuthenticationConverter} to use - * @since 6.5 - */ - public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { - Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); - this.authenticationConverter = authenticationConverter; - } - } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java index a5655a0c11..061f3b2232 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java @@ -27,6 +27,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -46,8 +47,14 @@ public class BearerTokenAuthenticationConverterTests { private static final String BEARER_TOKEN = "test_bearer_token"; + private final DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver(); + private final BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter(); + { + this.converter.setBearerTokenResolver(this.resolver); + } + @Test public void convertWhenAuthorizationHeaderIsPresentThenTokenIsConverted() { MockHttpServletRequest request = new MockHttpServletRequest(); @@ -64,7 +71,7 @@ public class BearerTokenAuthenticationConverterTests { request.setMethod(HttpMethod.GET.name()); request.addParameter("access_token", BEARER_TOKEN); - this.converter.setAllowUriQueryParameter(true); + this.resolver.setAllowUriQueryParameter(true); Authentication authentication = this.converter.convert(request); assertThat(authentication).isNotNull(); @@ -86,6 +93,7 @@ public class BearerTokenAuthenticationConverterTests { request.setMethod(HttpMethod.GET.name()); request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + BEARER_TOKEN); + this.resolver.setAllowUriQueryParameter(true); assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.converter.convert(request)) .withMessageContaining("Found multiple bearer tokens in the request"); } @@ -95,7 +103,7 @@ public class BearerTokenAuthenticationConverterTests { MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader(X_AUTH_TOKEN_HEADER, "Bearer " + TEST_X_AUTH_TOKEN); - this.converter.setBearerTokenHeaderName(X_AUTH_TOKEN_HEADER); + this.resolver.setBearerTokenHeaderName(X_AUTH_TOKEN_HEADER); Authentication authentication = this.converter.convert(request); assertThat(authentication).isNotNull(); @@ -140,7 +148,7 @@ public class BearerTokenAuthenticationConverterTests { request.setMethod(HttpMethod.POST.name()); request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); request.addParameter("access_token", BEARER_TOKEN); - this.converter.setAllowFormEncodedBodyParameter(true); + this.resolver.setAllowFormEncodedBodyParameter(true); assertThat(this.converter.convert(request)).isNotNull(); } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java index b64a29f762..67131e21b6 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java @@ -75,8 +75,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) public class BearerTokenAuthenticationFilterTests { - private static final String TEST_TOKEN = "token"; - @Mock AuthenticationEntryPoint authenticationEntryPoint; @@ -95,9 +93,6 @@ public class BearerTokenAuthenticationFilterTests { @Mock AuthenticationDetailsSource authenticationDetailsSource; - @Mock - AuthenticationConverter authenticationConverter; - MockHttpServletRequest request; MockHttpServletResponse response; @@ -269,6 +264,24 @@ public class BearerTokenAuthenticationFilterTests { assertThat(error.getDescription()).isEqualTo("Invalid bearer token"); } + @Test + public void doFilterWhenSetAuthenticationConverterAndAuthenticationDetailsSourceThenIllegalArgument( + @Mock AuthenticationConverter authenticationConverter) { + BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(this.authenticationManager, + authenticationConverter); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> filter.setAuthenticationDetailsSource(this.authenticationDetailsSource)); + } + + @Test + public void doFilterWhenSetBearerTokenResolverAndAuthenticationConverterThenIllegalArgument( + @Mock AuthenticationConverter authenticationConverter) { + BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(this.authenticationManager, + authenticationConverter); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> filter.setBearerTokenResolver(this.bearerTokenResolver)); + } + @Test public void setAuthenticationEntryPointWhenNullThenThrowsException() { BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(this.authenticationManager); @@ -302,9 +315,8 @@ public class BearerTokenAuthenticationFilterTests { @Test public void setConverterWhenNullThenThrowsException() { // @formatter:off - BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(this.authenticationManager); assertThatIllegalArgumentException() - .isThrownBy(() -> filter.setAuthenticationConverter(null)) + .isThrownBy(() -> new BearerTokenAuthenticationFilter(this.authenticationManager, null)) .withMessageContaining("authenticationConverter cannot be null"); // @formatter:on } @@ -327,171 +339,6 @@ public class BearerTokenAuthenticationFilterTests { // @formatter:on } - @Test - public void doFilterWhenBearerTokenPresentAndConverterSetThenAuthenticates() throws ServletException, IOException { - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - - filter.doFilter(this.request, this.response, this.filterChain); - - ArgumentCaptor captor = ArgumentCaptor - .forClass(BearerTokenAuthenticationToken.class); - verify(this.authenticationManager).authenticate(captor.capture()); - assertThat(captor.getValue().getPrincipal()).isEqualTo(TEST_TOKEN); - assertThat(this.request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME)) - .isNotNull(); - } - - @Test - public void doFilterWhenSecurityContextRepositoryAndConverterSetThenSaves() throws ServletException, IOException { - SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class); - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - TestingAuthenticationToken expectedAuthentication = new TestingAuthenticationToken("test", "password"); - given(this.authenticationManager.authenticate(any())).willReturn(expectedAuthentication); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - filter.setSecurityContextRepository(securityContextRepository); - - filter.doFilter(this.request, this.response, this.filterChain); - - ArgumentCaptor captor = ArgumentCaptor - .forClass(BearerTokenAuthenticationToken.class); - verify(this.authenticationManager).authenticate(captor.capture()); - assertThat(captor.getValue().getPrincipal()).isEqualTo(TEST_TOKEN); - ArgumentCaptor contextArg = ArgumentCaptor.forClass(SecurityContext.class); - verify(securityContextRepository).saveContext(contextArg.capture(), eq(this.request), eq(this.response)); - assertThat(contextArg.getValue().getAuthentication().getName()).isEqualTo(expectedAuthentication.getName()); - } - - @Test - public void doFilterWhenUsingAuthenticationManagerResolverAndConverterSetThenAuthenticates() throws Exception { - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManagerResolver)); - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - given(this.authenticationManagerResolver.resolve(any())).willReturn(this.authenticationManager); - - filter.doFilter(this.request, this.response, this.filterChain); - - ArgumentCaptor captor = ArgumentCaptor - .forClass(BearerTokenAuthenticationToken.class); - verify(this.authenticationManager).authenticate(captor.capture()); - assertThat(captor.getValue().getPrincipal()).isEqualTo(TEST_TOKEN); - assertThat(this.request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME)) - .isNotNull(); - } - - @Test - public void doFilterWhenNoBearerTokenPresentAndConverterSetThenDoesNotAuthenticate() - throws ServletException, IOException { - given(this.authenticationConverter.convert(this.request)).willReturn(null); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - - filter.doFilter(this.request, this.response, this.filterChain); - - verifyNoMoreInteractions(this.authenticationManager); - } - - @Test - public void doFilterWhenMalformedBearerTokenAndConverterSetThenPropagatesError() - throws ServletException, IOException { - BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST, - "description", "uri"); - OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); - given(this.authenticationConverter.convert(this.request)).willThrow(exception); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - filter.doFilter(this.request, this.response, this.filterChain); - - verifyNoMoreInteractions(this.authenticationManager); - verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); - } - - @Test - public void doFilterWhenAuthenticationFailsWithDefaultHandlerAndConverterSetThenPropagatesError() - throws ServletException, IOException { - BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, - "description", "uri"); - OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - given(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class))).willThrow(exception); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - - filter.doFilter(this.request, this.response, this.filterChain); - - verify(this.authenticationEntryPoint).commence(this.request, this.response, exception); - } - - @Test - public void doFilterWhenAuthenticationFailsWithCustomHandlerAndConverterSetThenPropagatesError() - throws ServletException, IOException { - BearerTokenError error = new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, - "description", "uri"); - OAuth2AuthenticationException exception = new OAuth2AuthenticationException(error); - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - given(this.authenticationManager.authenticate(any(BearerTokenAuthenticationToken.class))).willThrow(exception); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - filter.setAuthenticationFailureHandler(this.authenticationFailureHandler); - - filter.doFilter(this.request, this.response, this.filterChain); - - verify(this.authenticationFailureHandler).onAuthenticationFailure(this.request, this.response, exception); - } - - @Test - public void doFilterWhenConverterSetAndAuthenticationServiceExceptionThenRethrows() { - AuthenticationServiceException exception = new AuthenticationServiceException("message"); - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - given(this.authenticationManager.authenticate(any())).willThrow(exception); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - - assertThatExceptionOfType(AuthenticationServiceException.class) - .isThrownBy(() -> filter.doFilter(this.request, this.response, this.filterChain)); - } - - @Test - public void doFilterWhenConverterSetAndCustomEntryPointAndAuthenticationErrorThenUses() - throws ServletException, IOException { - AuthenticationException exception = new InvalidBearerTokenException("message"); - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - given(this.authenticationManager.authenticate(any())).willThrow(exception); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - AuthenticationEntryPoint entrypoint = mock(AuthenticationEntryPoint.class); - filter.setAuthenticationEntryPoint(entrypoint); - - filter.doFilter(this.request, this.response, this.filterChain); - - verify(entrypoint).commence(any(), any(), any(InvalidBearerTokenException.class)); - } - - @Test - public void doFilterWhenConverterSetCustomSecurityContextHolderStrategyThenUses() - throws ServletException, IOException { - given(this.authenticationConverter.convert(this.request)) - .willReturn(new BearerTokenAuthenticationToken(TEST_TOKEN)); - BearerTokenAuthenticationFilter filter = addMocksWithConverter( - new BearerTokenAuthenticationFilter(this.authenticationManager)); - SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); - given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl()); - filter.setSecurityContextHolderStrategy(strategy); - - filter.doFilter(this.request, this.response, this.filterChain); - - verify(strategy).setContext(any()); - } - private BearerTokenAuthenticationFilter addMocks(BearerTokenAuthenticationFilter filter) { filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); filter.setBearerTokenResolver(this.bearerTokenResolver); @@ -506,10 +353,4 @@ public class BearerTokenAuthenticationFilterTests { verifyNoMoreInteractions(this.authenticationManager); } - private BearerTokenAuthenticationFilter addMocksWithConverter(BearerTokenAuthenticationFilter filter) { - filter.setAuthenticationEntryPoint(this.authenticationEntryPoint); - filter.setAuthenticationConverter(this.authenticationConverter); - return filter; - } - } From a0c5504ecaad467f6f1fca2c99025ff825eb729c Mon Sep 17 00:00:00 2001 From: damable-nuvolex Date: Mon, 2 Jun 2025 14:03:46 -0500 Subject: [PATCH 07/10] Fix inconsistent constructor declaration Closes gh-16325 Signed-off-by: damable-nuvolex --- ...iveAuthorizationManagerMethodSecurityConfiguration.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index 7373d7b8c4..9ee76c3333 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -83,10 +83,11 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration private final AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeMethodInterceptor; - @Autowired(required = false) - ReactiveAuthorizationManagerMethodSecurityConfiguration(MethodSecurityExpressionHandler expressionHandler, + ReactiveAuthorizationManagerMethodSecurityConfiguration( + ObjectProvider expressionHandlers, ObjectProvider>> preAuthorizePostProcessor, ObjectProvider>> postAuthorizePostProcessor) { + MethodSecurityExpressionHandler expressionHandler = expressionHandlers.getIfUnique(); if (expressionHandler != null) { this.preFilterMethodInterceptor = new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler); this.preAuthorizeAuthorizationManager = new PreAuthorizeReactiveAuthorizationManager(expressionHandler); From 3b12e758d34f7074a89fe9342f1b2385f69f99b7 Mon Sep 17 00:00:00 2001 From: damable-nuvolex Date: Mon, 2 Jun 2025 14:03:46 -0500 Subject: [PATCH 08/10] Fix inconsistent constructor declaration Closes gh-16325 Signed-off-by: damable-nuvolex --- ...iveAuthorizationManagerMethodSecurityConfiguration.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index 7373d7b8c4..9ee76c3333 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -83,10 +83,11 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration private final AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeMethodInterceptor; - @Autowired(required = false) - ReactiveAuthorizationManagerMethodSecurityConfiguration(MethodSecurityExpressionHandler expressionHandler, + ReactiveAuthorizationManagerMethodSecurityConfiguration( + ObjectProvider expressionHandlers, ObjectProvider>> preAuthorizePostProcessor, ObjectProvider>> postAuthorizePostProcessor) { + MethodSecurityExpressionHandler expressionHandler = expressionHandlers.getIfUnique(); if (expressionHandler != null) { this.preFilterMethodInterceptor = new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler); this.preAuthorizeAuthorizationManager = new PreAuthorizeReactiveAuthorizationManager(expressionHandler); From dab989d7c31c5f588e27e07110c05a020d4a2880 Mon Sep 17 00:00:00 2001 From: Joe Grandja <10884212+jgrandja@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:23:57 -0400 Subject: [PATCH 09/10] Fix NPE with DPoP tokenAuthenticationManager Closes gh-17172 --- .../DPoPAuthenticationConfigurer.java | 23 ++++++++++++++++++- .../OAuth2ResourceServerConfigurer.java | 4 ++++ .../OAuth2ResourceServerConfigurerTests.java | 5 +++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java index cee89e0427..771d6c6e99 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java @@ -29,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; 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.core.Authentication; @@ -51,6 +52,9 @@ import org.springframework.security.web.context.RequestAttributeSecurityContextR import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +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 @@ -76,7 +80,7 @@ final class DPoPAuthenticationConfigurer> @Override public void configure(B http) { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); - http.authenticationProvider(new DPoPAuthenticationProvider(authenticationManager)); + http.authenticationProvider(new DPoPAuthenticationProvider(getTokenAuthenticationManager(http))); AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager, getAuthenticationConverter()); authenticationFilter.setRequestMatcher(getRequestMatcher()); @@ -87,6 +91,23 @@ final class DPoPAuthenticationConfigurer> 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); + }; + } + private RequestMatcher getRequestMatcher() { if (this.requestMatcher == null) { this.requestMatcher = new DPoPRequestMatcher(); 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 e9a425d46d..5add89675f 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 @@ -363,6 +363,10 @@ public final class OAuth2ResourceServerConfigurer getAuthenticationManagerResolver() { + return this.authenticationManagerResolver; + } + BearerTokenResolver getBearerTokenResolver() { if (this.bearerTokenResolver == null) { if (this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 6b263c7048..81b0a86498 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -88,6 +88,7 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -2532,7 +2533,9 @@ public class OAuth2ResourceServerConfigurerTests { // @formatter:off http .oauth2ResourceServer() - .authenticationManagerResolver(authenticationManagerResolver); + .authenticationManagerResolver(authenticationManagerResolver) + .and() + .anonymous(AbstractHttpConfigurer::disable); return http.build(); // @formatter:on } From b0f8aa5ea02d9abf1bc7b24e128e260cdbb63774 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Thu, 5 Jun 2025 17:17:42 +0300 Subject: [PATCH 10/10] Fix to allow multiple AuthenticationFilter instances to process each request Closes gh-17173 Signed-off-by: Andrey Litvitski --- .../authentication/AuthenticationFilter.java | 12 +++++++++++- .../AuthenticationFilterTests.java | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java index e4b752fd39..7a1a363f69 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -64,6 +64,7 @@ import org.springframework.web.filter.OncePerRequestFilter; * * * @author Sergey Bespalov + * @author Andrey Litvitski * @since 5.2.0 */ public class AuthenticationFilter extends OncePerRequestFilter { @@ -193,6 +194,15 @@ public class AuthenticationFilter extends OncePerRequestFilter { } } + @Override + protected String getAlreadyFilteredAttributeName() { + String name = getFilterName(); + if (name == null) { + name = getClass().getName().concat("-" + System.identityHashCode(this)); + } + return name + ALREADY_FILTERED_SUFFIX; + } + private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { this.securityContextHolderStrategy.clearContext(); diff --git a/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java index b4ab4411e4..8ad63e28bb 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -17,6 +17,7 @@ package org.springframework.security.web.authentication; import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; @@ -57,6 +58,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; /** * @author Sergey Bespalov + * @author Andrey Litvitski * @since 5.2.0 */ @ExtendWith(MockitoExtension.class) @@ -318,4 +320,18 @@ public class AuthenticationFilterTests { assertThat(securityContextArg.getValue().getAuthentication()).isEqualTo(authentication); } + @Test + public void filterWhenMultipleInChainThenAllFiltered() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + AuthenticationFilter filter1 = new AuthenticationFilter(this.authenticationManager, + this.authenticationConverter); + AuthenticationConverter converter2 = mock(AuthenticationConverter.class); + AuthenticationFilter filter2 = new AuthenticationFilter(this.authenticationManager, converter2); + FilterChain chain = new MockFilterChain(mock(Servlet.class), filter1, filter2); + chain.doFilter(request, response); + verify(this.authenticationConverter).convert(any()); + verify(converter2).convert(any()); + } + }