diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 03088dfebc..4f252d7912 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -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: * * @@ -106,19 +110,27 @@ public final class OAuth2ResourceServerConfigurer accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { + Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); + this.accessDeniedHandler = accessDeniedHandler; + return this; + } + + public OAuth2ResourceServerConfigurer authenticationEntryPoint(AuthenticationEntryPoint entryPoint) { + Assert.notNull(entryPoint, "entryPoint cannot be null"); + this.authenticationEntryPoint = entryPoint; + return this; + } + public OAuth2ResourceServerConfigurer bearerTokenResolver(BearerTokenResolver bearerTokenResolver) { Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); this.bearerTokenResolver = bearerTokenResolver; @@ -141,7 +153,7 @@ public final class OAuth2ResourceServerConfigurer exceptionHandling = http .getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index fbd63ad158..cae0e33423 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -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;