Bearer Token Exception Handling Configuration

This exposes #authenticationEntryPoint(), #accessDeniedHandler, on
the Resource Server DSL.

With these, a user can customize the error responses when a bearer
token request fails.

Fixes: gh-5497
This commit is contained in:
Josh Cummings 2018-07-17 11:18:16 -06:00
parent 6a45ecd4bb
commit fc5083ae0c
2 changed files with 200 additions and 8 deletions

View File

@ -34,6 +34,8 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthen
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
@ -48,6 +50,8 @@ import org.springframework.util.Assert;
* The following configuration options are available:
*
* <ul>
* <li>{@link #accessDeniedHandler(AccessDeniedHandler)}</li> - customizes how access denied errors are handled
* <li>{@link #authenticationEntryPoint(AuthenticationEntryPoint)}</li> - customizes how authentication failures are handled
* <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>
@ -106,19 +110,27 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
private BearerTokenResolver bearerTokenResolver;
private JwtConfigurer jwtConfigurer;
private AccessDeniedHandler accessDeniedHandler = new BearerTokenAccessDeniedHandler();
private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher();
private BearerTokenAuthenticationEntryPoint authenticationEntryPoint
= new BearerTokenAuthenticationEntryPoint();
private BearerTokenAccessDeniedHandler accessDeniedHandler
= new BearerTokenAccessDeniedHandler();
public OAuth2ResourceServerConfigurer(ApplicationContext context) {
Assert.notNull(context, "context cannot be null");
this.context = context;
}
public OAuth2ResourceServerConfigurer<H> accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
return this;
}
public OAuth2ResourceServerConfigurer<H> authenticationEntryPoint(AuthenticationEntryPoint entryPoint) {
Assert.notNull(entryPoint, "entryPoint cannot be null");
this.authenticationEntryPoint = entryPoint;
return this;
}
public OAuth2ResourceServerConfigurer<H> bearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
this.bearerTokenResolver = bearerTokenResolver;
@ -141,7 +153,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
@Override
public void init(H http) throws Exception {
registerDefaultDeniedHandler(http);
registerDefaultAccessDeniedHandler(http);
registerDefaultEntryPoint(http);
registerDefaultCsrfOverride(http);
}
@ -156,6 +168,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
BearerTokenAuthenticationFilter filter =
new BearerTokenAuthenticationFilter(manager);
filter.setBearerTokenResolver(bearerTokenResolver);
filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
filter = postProcess(filter);
http.addFilter(filter);
@ -211,7 +224,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
}
}
private void registerDefaultDeniedHandler(H http) {
private void registerDefaultAccessDeniedHandler(H http) {
ExceptionHandlingConfigurer<H> exceptionHandling = http
.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {

View File

@ -64,11 +64,17 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
@ -85,6 +91,7 @@ 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.hamcrest.core.StringStartsWith.startsWith;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -784,8 +791,101 @@ public class OAuth2ResourceServerConfigurerTests {
.isInstanceOf(NoUniqueBeanDefinitionException.class);
}
// -- exception handling
@Test
public void requestWhenRealmNameConfiguredThenUsesOnUnauthenticated()
throws Exception {
this.spring.register(RealmNameConfiguredOnEntryPoint.class, JwtDecoderConfig.class).autowire();
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
when(decoder.decode(anyString())).thenThrow(JwtException.class);
this.mvc.perform(get("/authenticated")
.with(bearerToken("invalid_token")))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\"")));
}
@Test
public void requestWhenRealmNameConfiguredThenUsesOnAccessDenied()
throws Exception {
this.spring.register(RealmNameConfiguredOnAccessDeniedHandler.class, JwtDecoderConfig.class).autowire();
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
when(decoder.decode(anyString())).thenReturn(JWT);
this.mvc.perform(get("/authenticated")
.with(bearerToken("insufficiently_scoped")))
.andExpect(status().isForbidden())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer realm=\"myRealm\"")));
}
@Test
public void authenticationEntryPointWhenGivenNullThenThrowsException() {
ApplicationContext context = mock(ApplicationContext.class);
OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context);
assertThatCode(() -> configurer.authenticationEntryPoint(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void accessDeniedHandlerWhenGivenNullThenThrowsException() {
ApplicationContext context = mock(ApplicationContext.class);
OAuth2ResourceServerConfigurer configurer = new OAuth2ResourceServerConfigurer(context);
assertThatCode(() -> configurer.accessDeniedHandler(null))
.isInstanceOf(IllegalArgumentException.class);
}
// -- In combination with other authentication providers
@Test
public void requestWhenBasicAndResourceServerEntryPointsThenMatchedByRequest()
throws Exception {
this.spring.register(BasicAndResourceServerConfig.class, JwtDecoderConfig.class).autowire();
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
when(decoder.decode(anyString())).thenThrow(JwtException.class);
this.mvc.perform(get("/authenticated")
.with(httpBasic("some", "user")))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic")));
this.mvc.perform(get("/authenticated"))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Basic")));
this.mvc.perform(get("/authenticated")
.with(bearerToken("invalid_token")))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer")));
}
@Test
public void requestWhenDefaultAndResourceServerAccessDeniedHandlersThenMatchedByRequest()
throws Exception {
this.spring.register(ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig.class,
JwtDecoderConfig.class).autowire();
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
when(decoder.decode(anyString())).thenReturn(JWT);
this.mvc.perform(get("/authenticated")
.with(httpBasic("basic-user", "basic-password")))
.andExpect(status().isForbidden())
.andExpect(header().doesNotExist(HttpHeaders.WWW_AUTHENTICATE));
this.mvc.perform(get("/authenticated")
.with(bearerToken("insufficiently_scoped")))
.andExpect(status().isForbidden())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer")));
}
@Test
public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages()
throws Exception {
@ -901,6 +1001,85 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class RealmNameConfiguredOnEntryPoint extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2()
.resourceServer()
.authenticationEntryPoint(authenticationEntryPoint())
.jwt();
// @formatter:on
}
AuthenticationEntryPoint authenticationEntryPoint() {
BearerTokenAuthenticationEntryPoint entryPoint =
new BearerTokenAuthenticationEntryPoint();
entryPoint.setRealmName("myRealm");
return entryPoint;
}
}
@EnableWebSecurity
static class RealmNameConfiguredOnAccessDeniedHandler extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().denyAll()
.and()
.oauth2()
.resourceServer()
.accessDeniedHandler(accessDeniedHandler())
.jwt();
// @formatter:on
}
AccessDeniedHandler accessDeniedHandler() {
BearerTokenAccessDeniedHandler accessDeniedHandler =
new BearerTokenAccessDeniedHandler();
accessDeniedHandler.setRealmName("myRealm");
return accessDeniedHandler;
}
}
@EnableWebSecurity
static class ExceptionHandlingAndResourceServerWithAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
.anyRequest().denyAll()
.and()
.exceptionHandling()
.defaultAccessDeniedHandlerFor(new AccessDeniedHandlerImpl(), request -> false)
.and()
.httpBasic()
.and()
.oauth2()
.resourceServer()
.jwt();
// @formatter:on
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder()
.username("basic-user")
.password("basic-password")
.roles("USER")
.build());
}
}
@EnableWebSecurity
static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri:https://example.org}") String uri;