Explorar o código

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
Josh Cummings %!s(int64=7) %!d(string=hai) anos
pai
achega
fc5083ae0c

+ 21 - 8
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:
  *
  * <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) {

+ 179 - 0
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;