From 6a45ecd4bbf17327d8734ff1f8f6ef4ef1079013 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 20 Jul 2018 14:05:27 -0600 Subject: [PATCH] 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 --- .../configurers/oauth2/OAuth2Configurer.java | 4 +- .../OAuth2ResourceServerConfigurer.java | 41 +++- .../OAuth2ResourceServerConfigurerTests.java | 230 +++++++++++++++++- 3 files changed, 260 insertions(+), 15 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java index 772e812297..433119b453 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java +++ b/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> } 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); } 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 ae3db1588b..03088dfebc 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 @@ -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: * * * @@ -99,7 +101,11 @@ import org.springframework.util.Assert; public final class OAuth2ResourceServerConfigurer> extends AbstractHttpConfigurer, 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 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 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) { 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 6b89689ad9..fbd63ad158 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 @@ -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("/")