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
This commit is contained in:
parent
ba29b363fc
commit
6a45ecd4bb
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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("/")
|
||||
|
|
Loading…
Reference in New Issue