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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user