Переглянути джерело

Bump io.spring.develocity.conventions from 0.0.22 to 0.0.23

Rob Winch 4 місяців тому
батько
коміт
3948440ee4
21 змінених файлів з 806 додано та 75 видалено
  1. 9 1
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyWebConfiguration.java
  2. 4 3
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java
  3. 22 1
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/DPoPAuthenticationConfigurer.java
  4. 70 20
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java
  5. 72 8
      config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java
  6. 3 0
      config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc
  7. 6 0
      config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd
  8. 84 8
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java
  9. 28 13
      config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java
  10. 32 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverter.xml
  11. 32 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverterAndBearerTokenResolver.xml
  12. 27 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockAuthenticationConverter.xml
  13. 55 0
      docs/modules/ROOT/pages/migration/servlet/oauth2.adoc
  14. 7 1
      docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc
  15. 2 2
      gradle/libs.versions.toml
  16. 72 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java
  17. 69 16
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java
  18. 156 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java
  19. 28 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java
  20. 11 1
      web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java
  21. 17 1
      web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java

+ 9 - 1
config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyWebConfiguration.java

@@ -26,6 +26,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.access.AccessDeniedException;
@@ -58,7 +59,9 @@ class AuthorizationProxyWebConfiguration implements WebMvcConfigurer {
 		resolvers.add(new AccessDeniedExceptionResolver());
 	}
 
-	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) {
@@ -81,6 +84,11 @@ class AuthorizationProxyWebConfiguration implements WebMvcConfigurer {
 			return null;
 		}
 
+		@Override
+		public int getOrder() {
+			return DEFAULT_ORDER;
+		}
+
 	}
 
 	static class AccessDeniedExceptionResolver implements HandlerExceptionResolver {

+ 4 - 3
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<MethodSecurityExpressionHandler> expressionHandlers,
 			ObjectProvider<ObjectPostProcessor<ReactiveAuthorizationManager<MethodInvocation>>> preAuthorizePostProcessor,
 			ObjectProvider<ObjectPostProcessor<ReactiveAuthorizationManager<MethodInvocationResult>>> postAuthorizePostProcessor) {
+		MethodSecurityExpressionHandler expressionHandler = expressionHandlers.getIfUnique();
 		if (expressionHandler != null) {
 			this.preFilterMethodInterceptor = new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
 			this.preAuthorizeAuthorizationManager = new PreAuthorizeReactiveAuthorizationManager(expressionHandler);

+ 22 - 1
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<B extends HttpSecurityBuilder<B>>
 	@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<B extends HttpSecurityBuilder<B>>
 		http.addFilter(authenticationFilter);
 	}
 
+	private AuthenticationManager getTokenAuthenticationManager(B http) {
+		OAuth2ResourceServerConfigurer<B> resourceServerConfigurer = http
+			.getConfigurer(OAuth2ResourceServerConfigurer.class);
+		final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver = resourceServerConfigurer
+			.getAuthenticationManagerResolver();
+		if (authenticationManagerResolver == null) {
+			return resourceServerConfigurer.getAuthenticationManager(http);
+		}
+		return (authentication) -> {
+			RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+			ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
+			AuthenticationManager authenticationManager = authenticationManagerResolver
+				.resolve(servletRequestAttributes.getRequest());
+			return authenticationManager.authenticate(authentication);
+		};
+	}
+
 	private RequestMatcher getRequestMatcher() {
 		if (this.requestMatcher == null) {
 			this.requestMatcher = new DPoPRequestMatcher();

+ 70 - 20
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

@@ -37,6 +37,7 @@ 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;
@@ -49,13 +50,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;
@@ -156,7 +158,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 	private AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
 
-	private BearerTokenResolver bearerTokenResolver;
+	private AuthenticationConverter authenticationConverter;
 
 	private JwtConfigurer jwtConfigurer;
 
@@ -196,7 +198,19 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 	public OAuth2ResourceServerConfigurer<H> bearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
 		Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
-		this.bearerTokenResolver = bearerTokenResolver;
+		this.authenticationConverter = new BearerTokenResolverHoldingAuthenticationConverter(bearerTokenResolver);
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationConverter} to use.
+	 * @param authenticationConverter the authentication converter
+	 * @return the {@link OAuth2ResourceServerConfigurer} for further configuration
+	 * @since 7.0
+	 */
+	public OAuth2ResourceServerConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationConverter = authenticationConverter;
 		return this;
 	}
 
@@ -271,16 +285,15 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 	@Override
 	public void configure(H http) {
-		BearerTokenResolver bearerTokenResolver = getBearerTokenResolver();
-		this.requestMatcher.setBearerTokenResolver(bearerTokenResolver);
 		AuthenticationManagerResolver resolver = this.authenticationManagerResolver;
 		if (resolver == null) {
 			AuthenticationManager authenticationManager = getAuthenticationManager(http);
 			resolver = (request) -> authenticationManager;
 		}
 
-		BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(resolver);
-		filter.setBearerTokenResolver(bearerTokenResolver);
+		AuthenticationConverter converter = getAuthenticationConverter();
+		this.requestMatcher.setAuthenticationConverter(converter);
+		BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(resolver, converter);
 		filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
 		filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
 		filter = postProcess(filter);
@@ -363,16 +376,33 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 		return http.getSharedObject(AuthenticationManager.class);
 	}
 
+	AuthenticationManagerResolver<HttpServletRequest> getAuthenticationManagerResolver() {
+		return this.authenticationManagerResolver;
+	}
+
+	AuthenticationConverter getAuthenticationConverter() {
+		if (this.authenticationConverter != null) {
+			return this.authenticationConverter;
+		}
+		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 BearerTokenResolverHoldingAuthenticationConverter(bearerTokenResolver);
+		}
+		else {
+			this.authenticationConverter = new BearerTokenAuthenticationConverter();
+		}
+		return this.authenticationConverter;
+	}
+
 	BearerTokenResolver getBearerTokenResolver() {
-		if (this.bearerTokenResolver == null) {
-			if (this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0) {
-				this.bearerTokenResolver = this.context.getBean(BearerTokenResolver.class);
-			}
-			else {
-				this.bearerTokenResolver = new DefaultBearerTokenResolver();
-			}
+		AuthenticationConverter authenticationConverter = getAuthenticationConverter();
+		if (authenticationConverter instanceof OAuth2ResourceServerConfigurer.BearerTokenResolverHoldingAuthenticationConverter bearer) {
+			return bearer.bearerTokenResolver;
 		}
-		return this.bearerTokenResolver;
+		return null;
 	}
 
 	public class JwtConfigurer {
@@ -560,21 +590,41 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 
 	private static final class BearerTokenRequestMatcher implements RequestMatcher {
 
-		private BearerTokenResolver bearerTokenResolver;
+		private AuthenticationConverter authenticationConverter;
 
 		@Override
 		public boolean matches(HttpServletRequest request) {
 			try {
-				return this.bearerTokenResolver.resolve(request) != null;
+				return this.authenticationConverter.convert(request) != null;
 			}
 			catch (OAuth2AuthenticationException ex) {
 				return false;
 			}
 		}
 
-		void setBearerTokenResolver(BearerTokenResolver tokenResolver) {
-			Assert.notNull(tokenResolver, "resolver cannot be null");
-			this.bearerTokenResolver = tokenResolver;
+		void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
+			Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+			this.authenticationConverter = authenticationConverter;
+		}
+
+	}
+
+	private static final class BearerTokenResolverHoldingAuthenticationConverter implements AuthenticationConverter {
+
+		private final BearerTokenResolver bearerTokenResolver;
+
+		private final AuthenticationConverter authenticationConverter;
+
+		BearerTokenResolverHoldingAuthenticationConverter(BearerTokenResolver bearerTokenResolver) {
+			this.bearerTokenResolver = bearerTokenResolver;
+			BearerTokenAuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter();
+			authenticationConverter.setBearerTokenResolver(bearerTokenResolver);
+			this.authenticationConverter = authenticationConverter;
+		}
+
+		@Override
+		public Authentication convert(HttpServletRequest request) {
+			return this.authenticationConverter.convert(request);
 		}
 
 	}

+ 72 - 8
config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.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.
@@ -23,6 +23,7 @@ import jakarta.servlet.http.HttpServletRequest;
 import org.w3c.dom.Element;
 
 import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.factory.BeanDefinitionStoreException;
 import org.springframework.beans.factory.FactoryBean;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanReference;
@@ -43,9 +44,10 @@ import org.springframework.security.oauth2.server.resource.authentication.Opaque
 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.authentication.AuthenticationConverter;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
@@ -64,6 +66,8 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 
 	static final String BEARER_TOKEN_RESOLVER_REF = "bearer-token-resolver-ref";
 
+	static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref";
+
 	static final String ENTRY_POINT_REF = "entry-point-ref";
 
 	static final String BEARER_TOKEN_RESOLVER = "bearerTokenResolver";
@@ -124,11 +128,16 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 					pc.getReaderContext().registerWithGeneratedName(opaqueTokenAuthenticationProvider)));
 		}
 		BeanMetadataElement bearerTokenResolver = getBearerTokenResolver(oauth2ResourceServer);
-		BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
-			.rootBeanDefinition(BearerTokenRequestMatcher.class);
-		requestMatcherBuilder.addConstructorArgValue(bearerTokenResolver);
-		BeanDefinition requestMatcher = requestMatcherBuilder.getBeanDefinition();
+		BeanMetadataElement authenticationConverter = getAuthenticationConverter(oauth2ResourceServer);
+		if (bearerTokenResolver != null && authenticationConverter != null) {
+			throw new BeanDefinitionStoreException(
+					"You cannot use bearer-token-ref and authentication-converter-ref in the same oauth2-resource-server element");
+		}
+		if (bearerTokenResolver == null && authenticationConverter == null) {
+			authenticationConverter = new RootBeanDefinition(BearerTokenAuthenticationConverter.class);
+		}
 		BeanMetadataElement authenticationEntryPoint = getEntryPoint(oauth2ResourceServer);
+		BeanDefinition requestMatcher = buildRequestMatcher(bearerTokenResolver, authenticationConverter);
 		this.entryPoints.put(requestMatcher, authenticationEntryPoint);
 		this.deniedHandlers.put(requestMatcher, this.accessDeniedHandler);
 		this.ignoreCsrfRequestMatchers.add(requestMatcher);
@@ -136,13 +145,35 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 			.rootBeanDefinition(BearerTokenAuthenticationFilter.class);
 		BeanMetadataElement authenticationManagerResolver = getAuthenticationManagerResolver(oauth2ResourceServer);
 		filterBuilder.addConstructorArgValue(authenticationManagerResolver);
-		filterBuilder.addPropertyValue(BEARER_TOKEN_RESOLVER, bearerTokenResolver);
 		filterBuilder.addPropertyValue(AUTHENTICATION_ENTRY_POINT, authenticationEntryPoint);
 		filterBuilder.addPropertyValue("securityContextHolderStrategy",
 				this.authenticationFilterSecurityContextHolderStrategy);
+
+		if (authenticationConverter != null) {
+			filterBuilder.addConstructorArgValue(authenticationConverter);
+		}
+		if (bearerTokenResolver != null) {
+			filterBuilder.addPropertyValue(BEARER_TOKEN_RESOLVER, bearerTokenResolver);
+		}
 		return filterBuilder.getBeanDefinition();
 	}
 
+	private BeanDefinition buildRequestMatcher(BeanMetadataElement bearerTokenResolver,
+			BeanMetadataElement authenticationConverter) {
+		if (bearerTokenResolver != null) {
+			BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
+				.rootBeanDefinition(BearerTokenRequestMatcher.class);
+			requestMatcherBuilder.addConstructorArgValue(bearerTokenResolver);
+			return requestMatcherBuilder.getBeanDefinition();
+		}
+		BeanDefinitionBuilder requestMatcherBuilder = BeanDefinitionBuilder
+			.rootBeanDefinition(BearerTokenAuthenticationRequestMatcher.class);
+		if (authenticationConverter != null) {
+			requestMatcherBuilder.addConstructorArgValue(authenticationConverter);
+		}
+		return requestMatcherBuilder.getBeanDefinition();
+	}
+
 	void validateConfiguration(Element oauth2ResourceServer, Element jwt, Element opaqueToken, ParserContext pc) {
 		if (!oauth2ResourceServer.hasAttribute(AUTHENTICATION_MANAGER_RESOLVER_REF)) {
 			if (jwt == null && opaqueToken == null) {
@@ -178,11 +209,19 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 	BeanMetadataElement getBearerTokenResolver(Element element) {
 		String bearerTokenResolverRef = element.getAttribute(BEARER_TOKEN_RESOLVER_REF);
 		if (!StringUtils.hasLength(bearerTokenResolverRef)) {
-			return new RootBeanDefinition(DefaultBearerTokenResolver.class);
+			return null;
 		}
 		return new RuntimeBeanReference(bearerTokenResolverRef);
 	}
 
+	BeanMetadataElement getAuthenticationConverter(Element element) {
+		String authenticationConverterRef = element.getAttribute(AUTHENTICATION_CONVERTER_REF);
+		if (!StringUtils.hasLength(authenticationConverterRef)) {
+			return null;
+		}
+		return new RuntimeBeanReference(authenticationConverterRef);
+	}
+
 	BeanMetadataElement getEntryPoint(Element element) {
 		String entryPointRef = element.getAttribute(ENTRY_POINT_REF);
 		if (!StringUtils.hasLength(entryPointRef)) {
@@ -366,4 +405,29 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa
 
 	}
 
+	static final class BearerTokenAuthenticationRequestMatcher implements RequestMatcher {
+
+		private final AuthenticationConverter authenticationConverter;
+
+		BearerTokenAuthenticationRequestMatcher() {
+			this.authenticationConverter = new BearerTokenAuthenticationConverter();
+		}
+
+		BearerTokenAuthenticationRequestMatcher(AuthenticationConverter authenticationConverter) {
+			Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+			this.authenticationConverter = authenticationConverter;
+		}
+
+		@Override
+		public boolean matches(HttpServletRequest request) {
+			try {
+				return this.authenticationConverter.convert(request) != null;
+			}
+			catch (OAuth2AuthenticationException ex) {
+				return false;
+			}
+		}
+
+	}
+
 }

+ 3 - 0
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

+ 6 - 0
config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd

@@ -1999,6 +1999,12 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="authentication-converter-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to a AuthenticationConverter
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
   </xs:attributeGroup>
   <xs:element name="jwt">
       <xs:annotation>

+ 84 - 8
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;
@@ -127,12 +128,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 +762,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 +1411,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 <T> void registerMockBean(GenericApplicationContext context, String name, Class<T> clazz) {
 		context.registerBean(name, clazz, () -> mock(clazz));
 	}
@@ -2516,6 +2553,43 @@ 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() {
+			DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
+			resolver.setAllowUriQueryParameter(true);
+			BearerTokenAuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter();
+			authenticationConverter.setBearerTokenResolver(resolver);
+			return authenticationConverter;
+		}
+
+		@Bean
+		AuthenticationConverter authenticationConverterTwo() {
+			DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
+			resolver.setAllowUriQueryParameter(true);
+			BearerTokenAuthenticationConverter authenticationConverter = new BearerTokenAuthenticationConverter();
+			authenticationConverter.setBearerTokenResolver(resolver);
+			return authenticationConverter;
+		}
+
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class MultipleIssuersConfig {
@@ -2532,7 +2606,9 @@ public class OAuth2ResourceServerConfigurerTests {
 			// @formatter:off
 			http
 				.oauth2ResourceServer()
-					.authenticationManagerResolver(authenticationManagerResolver);
+					.authenticationManagerResolver(authenticationManagerResolver)
+					.and()
+				.anonymous(AbstractHttpConfigurer::disable);
 			return http.build();
 			// @formatter:on
 		}

+ 28 - 13
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();

+ 32 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverter.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
+		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http>
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-resource-server authentication-converter-ref="authenticationConverter">
+			<jwt decoder-ref="decoder"/>
+		</oauth2-resource-server>
+	</http>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 32 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-AuthenticationConverterAndBearerTokenResolver.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
+		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http>
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-resource-server authentication-converter-ref="authenticationConverter" bearer-token-resolver-ref="bearerTokenResolver">
+			<jwt decoder-ref="decoder"/>
+		</oauth2-resource-server>
+	</http>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 27 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests-MockAuthenticationConverter.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
+		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<b:bean name="authenticationConverter" class="org.mockito.Mockito" factory-method="mock">
+		<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationConverter" type="java.lang.Class"/>
+	</b:bean>
+</b:beans>

+ 55 - 0
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)
+----
+======

+ 7 - 1
docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

@@ -1266,12 +1266,18 @@ 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**
 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.
+This cannot be used in conjunction with `bearer-token-resolver-ref`
+
 [[nsa-jwt]]
 == <jwt>
 Represents an OAuth 2.0 Resource Server that will authorize JWTs

+ 2 - 2
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"
@@ -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]
 

+ 72 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java

@@ -0,0 +1,72 @@
+/*
+ * 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.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.core.Authentication;
+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;
+import org.springframework.util.StringUtils;
+
+/**
+ * Implementation of {@link AuthenticationConverter}, that converts request to
+ * {@link BearerTokenAuthenticationToken}
+ *
+ * @author Max Batischev
+ * @author Josh Cummings
+ * @since 7.0
+ */
+public final class BearerTokenAuthenticationConverter implements AuthenticationConverter {
+
+	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
+
+	private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
+
+	@Override
+	public Authentication convert(HttpServletRequest 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;
+	}
+
+	public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
+		Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
+		this.bearerTokenResolver = bearerTokenResolver;
+	}
+
+	/**
+	 * Set the {@link AuthenticationDetailsSource} to use. Defaults to
+	 * {@link WebAuthenticationDetailsSource}.
+	 * @param authenticationDetailsSource the {@code AuthenticationDetailsSource} to use
+	 */
+	public void setAuthenticationDetailsSource(
+			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
+		Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
+		this.authenticationDetailsSource = authenticationDetailsSource;
+	}
+
+}

+ 69 - 16
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java

@@ -40,10 +40,12 @@ 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;
 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;
@@ -75,6 +77,8 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
 
 	private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
 
+	private final AuthenticationConverter authenticationConverter;
+
 	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
 		.getContextHolderStrategy();
 
@@ -83,10 +87,6 @@ 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<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
-
 	private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
 
 	/**
@@ -95,8 +95,7 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
 	 */
 	public BearerTokenAuthenticationFilter(
 			AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver) {
-		Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
-		this.authenticationManagerResolver = authenticationManagerResolver;
+		this(authenticationManagerResolver, new BearerTokenAuthenticationConverter());
 	}
 
 	/**
@@ -104,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<HttpServletRequest> authenticationManagerResolver,
+			AuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationManagerResolver = authenticationManagerResolver;
+		this.authenticationConverter = authenticationConverter;
 	}
 
 	/**
@@ -121,24 +155,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);
@@ -191,10 +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.bearerTokenResolver = bearerTokenResolver;
+		if (this.authenticationConverter instanceof BearerTokenAuthenticationConverter converter) {
+			converter.setBearerTokenResolver(bearerTokenResolver);
+		}
+		else {
+			throw new IllegalArgumentException(
+					"You cannot both specify an AuthenticationConverter and a BearerTokenResolver.");
+		}
 	}
 
 	/**
@@ -221,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<HttpServletRequest, ?> 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) {

+ 156 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverterTests.java

@@ -0,0 +1,156 @@
+/*
+ * 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 org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
+
+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 DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
+
+	private final BearerTokenAuthenticationConverter converter = new BearerTokenAuthenticationConverter();
+
+	{
+		this.converter.setBearerTokenResolver(this.resolver);
+	}
+
+	@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.resolver.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);
+
+		this.resolver.setAllowUriQueryParameter(true);
+		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.resolver.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<HttpServletRequest, ?> 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.resolver.setAllowFormEncodedBodyParameter(true);
+
+		assertThat(this.converter.convert(request)).isNotNull();
+	}
+
+}

+ 28 - 0
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;
@@ -263,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);
@@ -293,6 +312,15 @@ public class BearerTokenAuthenticationFilterTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void setConverterWhenNullThenThrowsException() {
+		// @formatter:off
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> new BearerTokenAuthenticationFilter(this.authenticationManager, null))
+				.withMessageContaining("authenticationConverter cannot be null");
+		// @formatter:on
+	}
+
 	@Test
 	public void constructorWhenNullAuthenticationManagerThenThrowsException() {
 		// @formatter:off

+ 11 - 1
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;
  * </ul>
  *
  * @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();

+ 17 - 1
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());
+	}
+
 }