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:
*
*
+ * - {@link #accessDeniedHandler(AccessDeniedHandler)}
- customizes how access denied errors are handled
+ * - {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
- customizes how authentication failures are handled
* - {@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request
* - {@link #jwt()} - enables Jwt-encoded bearer token support
*
@@ -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;