mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-28 06:42:49 +00:00
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.BearerTokenResolver;
|
||||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||||
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
|
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.security.web.util.matcher.RequestMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
@ -48,6 +50,8 @@ import org.springframework.util.Assert;
|
|||||||
* The following configuration options are available:
|
* The following configuration options are available:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <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 #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request</li>
|
||||||
* <li>{@link #jwt()} - enables Jwt-encoded bearer token support</li>
|
* <li>{@link #jwt()} - enables Jwt-encoded bearer token support</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@ -106,19 +110,27 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||||||
private BearerTokenResolver bearerTokenResolver;
|
private BearerTokenResolver bearerTokenResolver;
|
||||||
private JwtConfigurer jwtConfigurer;
|
private JwtConfigurer jwtConfigurer;
|
||||||
|
|
||||||
|
private AccessDeniedHandler accessDeniedHandler = new BearerTokenAccessDeniedHandler();
|
||||||
|
private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
|
||||||
private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher();
|
private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher();
|
||||||
|
|
||||||
private BearerTokenAuthenticationEntryPoint authenticationEntryPoint
|
|
||||||
= new BearerTokenAuthenticationEntryPoint();
|
|
||||||
|
|
||||||
private BearerTokenAccessDeniedHandler accessDeniedHandler
|
|
||||||
= new BearerTokenAccessDeniedHandler();
|
|
||||||
|
|
||||||
public OAuth2ResourceServerConfigurer(ApplicationContext context) {
|
public OAuth2ResourceServerConfigurer(ApplicationContext context) {
|
||||||
Assert.notNull(context, "context cannot be null");
|
Assert.notNull(context, "context cannot be null");
|
||||||
this.context = context;
|
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) {
|
public OAuth2ResourceServerConfigurer<H> bearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
|
||||||
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
|
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
|
||||||
this.bearerTokenResolver = bearerTokenResolver;
|
this.bearerTokenResolver = bearerTokenResolver;
|
||||||
@ -141,7 +153,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(H http) throws Exception {
|
public void init(H http) throws Exception {
|
||||||
registerDefaultDeniedHandler(http);
|
registerDefaultAccessDeniedHandler(http);
|
||||||
registerDefaultEntryPoint(http);
|
registerDefaultEntryPoint(http);
|
||||||
registerDefaultCsrfOverride(http);
|
registerDefaultCsrfOverride(http);
|
||||||
}
|
}
|
||||||
@ -156,6 +168,7 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
|
|||||||
BearerTokenAuthenticationFilter filter =
|
BearerTokenAuthenticationFilter filter =
|
||||||
new BearerTokenAuthenticationFilter(manager);
|
new BearerTokenAuthenticationFilter(manager);
|
||||||
filter.setBearerTokenResolver(bearerTokenResolver);
|
filter.setBearerTokenResolver(bearerTokenResolver);
|
||||||
|
filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
|
||||||
filter = postProcess(filter);
|
filter = postProcess(filter);
|
||||||
|
|
||||||
http.addFilter(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
|
ExceptionHandlingConfigurer<H> exceptionHandling = http
|
||||||
.getConfigurer(ExceptionHandlingConfigurer.class);
|
.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||||
if (exceptionHandling == null) {
|
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.Jwt;
|
||||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
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.jwt.NimbusJwtDecoderJwkSupport;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
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.BearerTokenResolver;
|
||||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
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.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.MockMvc;
|
||||||
import org.springframework.test.web.servlet.MvcResult;
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
import org.springframework.test.web.servlet.ResultMatcher;
|
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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
import static org.hamcrest.CoreMatchers.containsString;
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
import static org.hamcrest.core.StringStartsWith.startsWith;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@ -784,8 +791,101 @@ public class OAuth2ResourceServerConfigurerTests {
|
|||||||
.isInstanceOf(NoUniqueBeanDefinitionException.class);
|
.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
|
// -- 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
|
@Test
|
||||||
public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages()
|
public void getWhenAlsoUsingHttpBasicThenCorrectProviderEngages()
|
||||||
throws Exception {
|
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
|
@EnableWebSecurity
|
||||||
static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter {
|
static class BasicAndResourceServerConfig extends WebSecurityConfigurerAdapter {
|
||||||
@Value("${mock.jwk-set-uri:https://example.org}") String uri;
|
@Value("${mock.jwk-set-uri:https://example.org}") String uri;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user