From 10feecbd08bc52b73dcac87e8e5876ce0e63a49e Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 8 May 2023 18:11:10 -0500 Subject: [PATCH] Fix redirect to login page for token requests Fixes an issue where auto-configuration for Spring Authorization Server was overriding the default exception handling (AuthenticationEntryPoint) resulting in anonymous requests to the token endpoint being redirected to the Spring Security login page instead of returning 401 Unauthorized. Auto-configuration now registers a defaultAuthenticationEntryPointFor that is added to any other entry points already configured. See gh-35368 --- ...izationServerWebSecurityConfiguration.java | 15 +++++- ...h2AuthorizationServerApplicationTests.java | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java index c3e60f807e..5c36f2b462 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerWebSecurityConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.security.oauth2.server.servlet; +import java.util.Set; + import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; import org.springframework.boot.autoconfigure.security.SecurityProperties; @@ -23,6 +25,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; @@ -30,6 +33,8 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import static org.springframework.security.config.Customizer.withDefaults; @@ -49,8 +54,8 @@ class OAuth2AuthorizationServerWebSecurityConfiguration { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(withDefaults()); http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); - http.exceptionHandling( - (exceptions) -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))); + http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("/login"), createRequestMatcher())); return http.build(); } @@ -61,4 +66,10 @@ class OAuth2AuthorizationServerWebSecurityConfiguration { return http.build(); } + private static RequestMatcher createRequestMatcher() { + MediaTypeRequestMatcher requestMatcher = new MediaTypeRequestMatcher(MediaType.TEXT_HTML); + requestMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL)); + return requestMatcher; + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java index ce2d7b75f0..0ecf890c9f 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java @@ -17,6 +17,7 @@ package smoketest.oauth2.server; import java.net.URI; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -31,6 +32,7 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -118,4 +120,49 @@ class SampleOAuth2AuthorizationServerApplicationTests { .isEqualTo(OAuth2AccessToken.TokenType.BEARER.getValue()); } + @Test + void anonymousTokenRequestShouldReturnUnauthorized() { + HttpHeaders headers = new HttpHeaders(); + HttpEntity request = new HttpEntity<>(headers); + String requestUri = UriComponentsBuilder.fromUriString("/token") + .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") + .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") + .toUriString(); + ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + MAP_TYPE_REFERENCE); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void anonymousTokenRequestWithAcceptHeaderAllShouldReturnUnauthorized() { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(List.of(MediaType.ALL)); + HttpEntity request = new HttpEntity<>(headers); + String requestUri = UriComponentsBuilder.fromUriString("/token") + .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") + .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") + .toUriString(); + ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + MAP_TYPE_REFERENCE); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void anonymousTokenRequestWithAcceptHeaderTextHtmlShouldRedirectToLogin() { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(List.of(MediaType.TEXT_HTML)); + HttpEntity request = new HttpEntity<>(headers); + String requestUri = UriComponentsBuilder.fromUriString("/token") + .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") + .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) + .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") + .toUriString(); + ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + MAP_TYPE_REFERENCE); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); + assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create("http://localhost:" + this.port + "/login")); + } + }