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:
parent
6a45ecd4bb
commit
fc5083ae0c
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue