2
0
Эх сурвалжийг харах

Bearer Token Resolver Configuration

This introduces #bearerTokenResolver(BearerTokenResolver) to the
Resource Server DSL, allowing users to configure the resolver to allow
the access token as part of the request body or a query parameter. It
also allows the user to replace the resolver with a completely custom
one.

This also introduces the same ability by exposing a bean of type
BearerTokenResolver

Fixes: gh-5496
Josh Cummings 7 жил өмнө
parent
commit
6a45ecd4bb

+ 3 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java

@@ -16,6 +16,7 @@
 package org.springframework.security.config.annotation.web.configurers.oauth2;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -96,7 +97,8 @@ public final class OAuth2Configurer<B extends HttpSecurityBuilder<B>>
 	}
 
 	private void initResourceServerConfigurer() {
-		this.resourceServerConfigurer = new OAuth2ResourceServerConfigurer<>();
+		ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
+		this.resourceServerConfigurer = new OAuth2ResourceServerConfigurer<>(context);
 		this.resourceServerConfigurer.setBuilder(this.getBuilder());
 		this.resourceServerConfigurer.addObjectPostProcessor(this.objectPostProcessor);
 	}

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

@@ -25,6 +25,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.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
@@ -47,6 +48,7 @@ import org.springframework.util.Assert;
  * The following configuration options are available:
  *
  * <ul>
+ * <li>{@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request</li>
  * <li>{@link #jwt()} - enables Jwt-encoded bearer token support</li>
  * </ul>
  *
@@ -99,7 +101,11 @@ import org.springframework.util.Assert;
 public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<H>> extends
 		AbstractHttpConfigurer<OAuth2ResourceServerConfigurer<H>, H> {
 
-	private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
+	private final ApplicationContext context;
+
+	private BearerTokenResolver bearerTokenResolver;
+	private JwtConfigurer jwtConfigurer;
+
 	private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher();
 
 	private BearerTokenAuthenticationEntryPoint authenticationEntryPoint
@@ -108,12 +114,20 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 	private BearerTokenAccessDeniedHandler accessDeniedHandler
 			= new BearerTokenAccessDeniedHandler();
 
-	private JwtConfigurer jwtConfigurer;
+	public OAuth2ResourceServerConfigurer(ApplicationContext context) {
+		Assert.notNull(context, "context cannot be null");
+		this.context = context;
+	}
+
+	public OAuth2ResourceServerConfigurer<H> bearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
+		Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
+		this.bearerTokenResolver = bearerTokenResolver;
+		return this;
+	}
 
 	public JwtConfigurer jwt() {
 		if ( this.jwtConfigurer == null ) {
-			ApplicationContext context = this.getBuilder().getSharedObject(ApplicationContext.class);
-			this.jwtConfigurer = new JwtConfigurer(context);
+			this.jwtConfigurer = new JwtConfigurer(this.context);
 		}
 
 		return this.jwtConfigurer;
@@ -231,17 +245,28 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
 		csrf.ignoringRequestMatchers(this.requestMatcher);
 	}
 
-	private BearerTokenResolver getBearerTokenResolver() {
+	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();
+			}
+		}
+
 		return this.bearerTokenResolver;
 	}
 
 	private static final class BearerTokenRequestMatcher implements RequestMatcher {
-		private BearerTokenResolver bearerTokenResolver
-				= new DefaultBearerTokenResolver();
+		private BearerTokenResolver bearerTokenResolver;
 
 		@Override
 		public boolean matches(HttpServletRequest request) {
-			return this.bearerTokenResolver.resolve(request) != null;
+			try {
+				return this.bearerTokenResolver.resolve(request) != null;
+			} catch ( OAuth2AuthenticationException e ) {
+				return false;
+			}
 		}
 
 		public void setBearerTokenResolver(BearerTokenResolver tokenResolver) {

+ 224 - 6
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

@@ -66,6 +66,8 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
+import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MvcResult;
@@ -82,9 +84,11 @@ import org.springframework.web.context.support.GenericWebApplicationContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -526,6 +530,134 @@ public class OAuth2ResourceServerConfigurerTests {
 		assertThat(result.getRequest().getSession(false)).isNotNull();
 	}
 
+	// -- custom bearer token resolver
+
+	@Test
+	public void requestWhenBearerTokenResolverAllowsRequestBodyThenEitherHeaderOrRequestBodyIsAccepted()
+			throws Exception {
+
+		this.spring.register(AllowBearerTokenInRequestBodyConfig.class, JwtDecoderConfig.class,
+				BasicController.class).autowire();
+
+		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
+		when(decoder.decode(anyString())).thenReturn(JWT);
+
+		this.mvc.perform(get("/authenticated")
+				.with(bearerToken(JWT_TOKEN)))
+				.andExpect(status().isOk())
+				.andExpect(content().string(JWT_SUBJECT));
+
+		this.mvc.perform(post("/authenticated")
+				.param("access_token", JWT_TOKEN))
+				.andExpect(status().isOk())
+				.andExpect(content().string(JWT_SUBJECT));
+	}
+
+	@Test
+	public void requestWhenBearerTokenResolverAllowsQueryParameterThenEitherHeaderOrQueryParameterIsAccepted()
+			throws Exception {
+
+		this.spring.register(AllowBearerTokenAsQueryParameterConfig.class, JwtDecoderConfig.class,
+				BasicController.class).autowire();
+
+		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
+		when(decoder.decode(anyString())).thenReturn(JWT);
+
+		this.mvc.perform(get("/authenticated")
+				.with(bearerToken(JWT_TOKEN)))
+				.andExpect(status().isOk())
+				.andExpect(content().string(JWT_SUBJECT));
+
+		this.mvc.perform(get("/authenticated")
+				.param("access_token", JWT_TOKEN))
+				.andExpect(status().isOk())
+				.andExpect(content().string(JWT_SUBJECT));
+	}
+
+	@Test
+	public void requestWhenBearerTokenResolverAllowsRequestBodyAndRequestContainsTwoTokensThenInvalidRequest()
+			throws Exception {
+
+		this.spring.register(AllowBearerTokenInRequestBodyConfig.class, JwtDecoderConfig.class,
+				BasicController.class).autowire();
+
+		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
+		when(decoder.decode(anyString())).thenReturn(JWT);
+
+		this.mvc.perform(post("/authenticated")
+				.param("access_token", JWT_TOKEN)
+				.with(bearerToken(JWT_TOKEN))
+				.with(csrf()))
+				.andExpect(status().isBadRequest())
+				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("invalid_request")));
+	}
+
+	@Test
+	public void requestWhenBearerTokenResolverAllowsQueryParameterAndRequestContainsTwoTokensThenInvalidRequest()
+			throws Exception {
+
+		this.spring.register(AllowBearerTokenAsQueryParameterConfig.class, JwtDecoderConfig.class,
+				BasicController.class).autowire();
+
+		JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
+		when(decoder.decode(anyString())).thenReturn(JWT);
+
+		this.mvc.perform(get("/authenticated")
+				.with(bearerToken(JWT_TOKEN))
+				.param("access_token", JWT_TOKEN))
+				.andExpect(status().isBadRequest())
+				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("invalid_request")));
+	}
+
+	@Test
+	public void getBearerTokenResolverWhenDuplicateResolverBeansAndAnotherOnTheDslThenTheDslOneIsUsed() {
+		BearerTokenResolver resolverBean = mock(BearerTokenResolver.class);
+		BearerTokenResolver resolver = mock(BearerTokenResolver.class);
+
+		GenericWebApplicationContext context = new GenericWebApplicationContext();
+		context.registerBean("resolverOne", BearerTokenResolver.class, () -> resolverBean);
+		context.registerBean("resolverTwo", BearerTokenResolver.class, () -> resolverBean);
+		this.spring.context(context).autowire();
+
+		OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
+
+		oauth2.bearerTokenResolver(resolver);
+
+		assertThat(oauth2.getBearerTokenResolver()).isEqualTo(resolver);
+	}
+
+	@Test
+	public void getBearerTokenResolverWhenDuplicateResolverBeansThenWiringException() {
+		assertThatCode(() -> this.spring.register(MultipleBearerTokenResolverBeansConfig.class).autowire())
+				.isInstanceOf(BeanCreationException.class)
+				.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class);
+	}
+
+	@Test
+	public void getBearerTokenResolverWhenResolverBeanAndAnotherOnTheDslThenTheDslOneIsUsed() {
+		BearerTokenResolver resolver = mock(BearerTokenResolver.class);
+		BearerTokenResolver resolverBean = mock(BearerTokenResolver.class);
+
+		GenericWebApplicationContext context = new GenericWebApplicationContext();
+		context.registerBean(BearerTokenResolver.class, () -> resolverBean);
+		this.spring.context(context).autowire();
+
+		OAuth2ResourceServerConfigurer oauth2 = new OAuth2ResourceServerConfigurer(context);
+		oauth2.bearerTokenResolver(resolver);
+
+		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);
+	}
+
 	// -- custom jwt decoder
 
 	@Test
@@ -563,8 +695,10 @@ public class OAuth2ResourceServerConfigurerTests {
 
 	@Test
 	public void getJwtDecoderWhenConfiguredWithDecoderAndJwkSetUriThenLastOneWins() {
+		ApplicationContext context = mock(ApplicationContext.class);
+
 		OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
-				new OAuth2ResourceServerConfigurer().new JwtConfigurer(null);
+				new OAuth2ResourceServerConfigurer(context).jwt();
 
 		JwtDecoder decoder = mock(JwtDecoder.class);
 
@@ -574,7 +708,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder);
 
 		jwtConfigurer =
-				new OAuth2ResourceServerConfigurer().new JwtConfigurer(null);
+				new OAuth2ResourceServerConfigurer(context).jwt();
 
 		jwtConfigurer.decoder(decoder);
 		jwtConfigurer.jwkSetUri(JWK_SET_URI);
@@ -593,7 +727,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		when(context.getBean(JwtDecoder.class)).thenReturn(decoderBean);
 
 		OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
-				new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
+				new OAuth2ResourceServerConfigurer(context).jwt();
 		jwtConfigurer.decoder(decoder);
 
 		assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder);
@@ -607,7 +741,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		when(context.getBean(JwtDecoder.class)).thenReturn(decoder);
 
 		OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
-				new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
+				new OAuth2ResourceServerConfigurer(context).jwt();
 
 		jwtConfigurer.jwkSetUri(JWK_SET_URI);
 
@@ -627,7 +761,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		this.spring.context(context).autowire();
 
 		OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
-				new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
+				new OAuth2ResourceServerConfigurer(context).jwt();
 		jwtConfigurer.decoder(decoder);
 
 		assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder);
@@ -644,7 +778,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		this.spring.context(context).autowire();
 
 		OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
-				new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
+				new OAuth2ResourceServerConfigurer(context).jwt();
 
 		assertThatCode(() -> jwtConfigurer.getJwtDecoder())
 				.isInstanceOf(NoUniqueBeanDefinitionException.class);
@@ -833,6 +967,82 @@ public class OAuth2ResourceServerConfigurerTests {
 		}
 	}
 
+	@EnableWebSecurity
+	static class AllowBearerTokenInRequestBodyConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests()
+					.anyRequest().authenticated()
+					.and()
+				.oauth2()
+					.resourceServer()
+						.bearerTokenResolver(allowRequestBody())
+						.jwt();
+			// @formatter:on
+		}
+
+		private BearerTokenResolver allowRequestBody() {
+			DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
+			resolver.setAllowFormEncodedBodyParameter(true);
+			return resolver;
+		}
+	}
+
+	@EnableWebSecurity
+	static class AllowBearerTokenAsQueryParameterConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests()
+					.anyRequest().authenticated()
+					.and()
+				.oauth2()
+					.resourceServer()
+						.jwt();
+			// @formatter:on
+		}
+
+		@Bean
+		BearerTokenResolver allowQueryParameter() {
+			DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
+			resolver.setAllowUriQueryParameter(true);
+			return resolver;
+		}
+	}
+
+	@EnableWebSecurity
+	static class MultipleBearerTokenResolverBeansConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeRequests()
+					.anyRequest().authenticated()
+					.and()
+				.oauth2()
+					.resourceServer()
+						.jwt();
+			// @formatter:on
+		}
+
+		@Bean
+		BearerTokenResolver resolverOne() {
+			DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
+			resolver.setAllowUriQueryParameter(true);
+			return resolver;
+		}
+
+		@Bean
+		BearerTokenResolver resolverTwo() {
+			DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
+			resolver.setAllowFormEncodedBodyParameter(true);
+			return resolver;
+		}
+	}
+
 	@EnableWebSecurity
 	static class CustomJwtDecoderOnDsl extends WebSecurityConfigurerAdapter {
 		JwtDecoder decoder = mock(JwtDecoder.class);
@@ -877,6 +1087,14 @@ public class OAuth2ResourceServerConfigurerTests {
 		}
 	}
 
+	@Configuration
+	static class JwtDecoderConfig {
+		@Bean
+		public JwtDecoder jwtDecoder() {
+			return mock(JwtDecoder.class);
+		}
+	}
+
 	@RestController
 	static class BasicController {
 		@GetMapping("/")