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 e9a425d46d2..c4b976bc99e 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 @@ -37,6 +37,7 @@ 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.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 authenticationManagerResolver; - private BearerTokenResolver bearerTokenResolver; + private AuthenticationConverter authenticationConverter; private JwtConfigurer jwtConfigurer; @@ -196,7 +198,19 @@ public OAuth2ResourceServerConfigurer authenticationManagerResolver( public OAuth2ResourceServerConfigurer 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 authenticationConverter(AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; return this; } @@ -271,16 +285,15 @@ public void init(H http) { @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,29 @@ AuthenticationManager getAuthenticationManager(H http) { return http.getSharedObject(AuthenticationManager.class); } + 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 +586,41 @@ AuthenticationManager getAuthenticationManager(H http) { 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); } } diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java index 4fd3d12948d..78c9f0b4f79 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParser.java +++ b/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 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.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 @@ public BeanDefinition parse(Element oauth2ResourceServer, ParserContext pc) { 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 @@ public BeanDefinition parse(Element oauth2ResourceServer, ParserContext pc) { .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 @@ BeanMetadataElement getAuthenticationManagerResolver(Element element) { 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 @@ public boolean matches(HttpServletRequest request) { } + 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; + } + } + + } + } 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 15d15b191b7..bbf8622dfe2 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 34556b5549a..2e3d6cf2758 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 6b263c7048d..23dde67586b 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.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 void getBearerTokenResolverWhenResolverBeanAndAnotherOnTheDslThenTheDslOn 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 void getWhenCustomAuthenticationConverterThenUsed() throws Exception { 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,43 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } + @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 { 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 6a01051ca46..5ad167eef84 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.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.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.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 void getWhenCustomBearerTokenResolverThenUses() throws Exception { 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 void requestWhenBearerTokenResolverAllowsQueryParameterAndRequestContains // @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 void configureWhenDecoderAndJwkSetUriThenException() { .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 00000000000..04d3932f097 --- /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 00000000000..c0cb49bf651 --- /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 00000000000..397c4c59bf5 --- /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/migration/servlet/oauth2.adoc b/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc index 6cdb9043ddf..293abadddb9 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 2b434d43030..8979d5ad290 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/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]] == 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/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 00000000000..9f7e91a40a3 --- /dev/null +++ b/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 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 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 9cad61d0cb0..6a5f5c4869a 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,10 +40,12 @@ 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 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 authenticationDetailsSource = new WebAuthenticationDetailsSource(); - private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository(); /** @@ -95,8 +95,7 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter { */ public BearerTokenAuthenticationFilter( AuthenticationManagerResolver authenticationManagerResolver) { - Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null"); - this.authenticationManagerResolver = authenticationManagerResolver; + this(authenticationManagerResolver, new BearerTokenAuthenticationConverter()); } /** @@ -104,8 +103,43 @@ public BearerTokenAuthenticationFilter( * @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; } /** @@ -121,24 +155,22 @@ public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManag @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 void setSecurityContextRepository(SecurityContextRepository securityConte * 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 void setAuthenticationFailureHandler(final AuthenticationFailureHandler a /** * 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) { 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 00000000000..061f3b22329 --- /dev/null +++ b/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 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(); + } + +} 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 cc7477684f7..67131e21b65 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.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 void doFilterWhenDPoPBoundTokenDowngradedThenPropagatesError() throws Ser 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 void setAuthenticationConverterWhenNullThenThrowsException() { // @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