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

@@ -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;