From d9d161cd6b4e5fd621072daaff403260b9536fc5 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 15 Dec 2021 12:28:19 -0800 Subject: [PATCH] Allow previously authorized users to access the error page Prior to this commit, the `ErrorPageSecurityFilter` verified if access to the error page was allowed by invoking the `WebInvocationPrivilegeEvaluator` with the Authentication from the `SecurityContextHolder`. This meant that access to the error page was denied for a `null` Authentication or `AnonymousAuthenticationToken` in cases where the error page required authenticated access. This prevented authorized users from accessing the error page in case the Authentication wasn't retrievable for the error dispatch, which is the case for `@Transient` authentication or stateless session policy. This commit updates the `ErrorPageSecurityFilter` to check access to the error page only if the error is an authn or authz error in cases where an authentication object is not found in the SecurityContextHolder. This makes the error response consistent when bad credentials or no credentials are used while also allowing access to previously authorized users. Fixes gh-28953 --- .../filter/ErrorPageSecurityFilter.java | 27 ++-- .../filter/ErrorPageSecurityFilterTests.java | 12 ++ ...ctuatorCustomSecurityApplicationTests.java | 3 +- .../secure/SampleWebSecureApplication.java | 21 --- .../web/secure/AbstractErrorPageTests.java | 127 +++++++++++++++++ .../smoketest/web/secure/ErrorPageTests.java | 85 +++--------- .../web/secure/NoSessionErrorPageTests.java | 54 ++++++++ .../SampleWebSecureApplicationTests.java | 23 +++- .../secure/UnauthenticatedErrorPageTests.java | 128 ++++++++++++++++++ 9 files changed, 381 insertions(+), 99 deletions(-) create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/NoSessionErrorPageTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/UnauthenticatedErrorPageTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilter.java index 272451106f..16bbc6bf93 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilter.java @@ -31,6 +31,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; @@ -67,17 +68,28 @@ public class ErrorPageSecurityFilter implements Filter { private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - if (DispatcherType.ERROR.equals(request.getDispatcherType()) && !isAllowed(request)) { - sendError(request, response); + Integer errorCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + if (DispatcherType.ERROR.equals(request.getDispatcherType()) && !isAllowed(request, errorCode)) { + response.sendError((errorCode != null) ? errorCode : 401); return; } chain.doFilter(request, response); } - private boolean isAllowed(HttpServletRequest request) { - String uri = request.getRequestURI(); + private boolean isAllowed(HttpServletRequest request, Integer errorCode) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return getPrivilegeEvaluator().isAllowed(uri, authentication); + if (isUnauthenticated(authentication) && isNotAuthenticationError(errorCode)) { + return true; + } + return getPrivilegeEvaluator().isAllowed(request.getRequestURI(), authentication); + } + + private boolean isUnauthenticated(Authentication authentication) { + return (authentication == null || authentication instanceof AnonymousAuthenticationToken); + } + + private boolean isNotAuthenticationError(Integer errorCode) { + return (errorCode == null || (errorCode != 401 && errorCode != 403)); } private WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() { @@ -98,11 +110,6 @@ public class ErrorPageSecurityFilter implements Filter { } } - private void sendError(HttpServletRequest request, HttpServletResponse response) throws IOException { - Integer errorCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - response.sendError((errorCode != null) ? errorCode : 401); - } - /** * {@link WebInvocationPrivilegeEvaluator} that always allows access. */ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilterTests.java index bfa5dfebf6..b44cf0e6bd 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilterTests.java @@ -20,6 +20,7 @@ import javax.servlet.DispatcherType; import javax.servlet.FilterChain; import javax.servlet.RequestDispatcher; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,6 +28,9 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import static org.assertj.core.api.Assertions.assertThat; @@ -64,6 +68,11 @@ class ErrorPageSecurityFilterTests { this.securityFilter = new ErrorPageSecurityFilter(this.context); } + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + @Test void whenAccessIsAllowedShouldContinueDownFilterChain() throws Exception { given(this.privilegeEvaluator.isAllowed(anyString(), any())).willReturn(true); @@ -83,6 +92,9 @@ class ErrorPageSecurityFilterTests { @Test void whenAccessIsDeniedAndNoErrorCodeAttributeOnRequest() throws Exception { given(this.privilegeEvaluator.isAllowed(anyString(), any())).willReturn(false); + SecurityContext securityContext = mock(SecurityContext.class); + SecurityContextHolder.setContext(securityContext); + given(securityContext.getAuthentication()).willReturn(mock(Authentication.class)); this.securityFilter.doFilter(this.request, this.response, this.filterChain); verifyNoInteractions(this.filterChain); assertThat(this.response.getStatus()).isEqualTo(401); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java index 27fa0703a5..841e9c9646 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-custom-security/src/test/java/smoketest/actuator/customsecurity/SampleActuatorCustomSecurityApplicationTests.java @@ -66,7 +66,8 @@ class SampleActuatorCustomSecurityApplicationTests extends AbstractSampleActuato void testInsecureApplicationPath() { ResponseEntity entity = restTemplate().getForEntity(getPath() + "/foo", Map.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(entity.getBody()).isNull(); + Map body = entity.getBody(); + assertThat((String) body.get("message")).contains("Expected exception in controller"); } @Test diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/main/java/smoketest/web/secure/SampleWebSecureApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/main/java/smoketest/web/secure/SampleWebSecureApplication.java index b74d0337a6..e60992adbd 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/main/java/smoketest/web/secure/SampleWebSecureApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/main/java/smoketest/web/secure/SampleWebSecureApplication.java @@ -18,10 +18,6 @@ package smoketest.web.secure; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -38,21 +34,4 @@ public class SampleWebSecureApplication implements WebMvcConfigurer { new SpringApplicationBuilder(SampleWebSecureApplication.class).run(args); } - @Configuration(proxyBeanMethods = false) - protected static class ApplicationSecurity { - - @Bean - SecurityFilterChain configure(HttpSecurity http) throws Exception { - http.csrf().disable(); - http.authorizeRequests((requests) -> { - requests.antMatchers("/public/**").permitAll(); - requests.anyRequest().fullyAuthenticated(); - }); - http.httpBasic(); - http.formLogin((form) -> form.loginPage("/login").permitAll()); - return http.build(); - } - - } - } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java new file mode 100644 index 0000000000..2affea3b3b --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.web.secure; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Abstract base class for tests to ensure that the error page is accessible only to + * authorized users. + * + * @author Madhura Bhave + */ +abstract class AbstractErrorPageTests { + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void testBadCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrongpassword") + .exchange("/test", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse).isNull(); + } + + @Test + void testNoCredentials() { + final ResponseEntity response = this.testRestTemplate.exchange("/test", HttpMethod.GET, null, + JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse).isNull(); + } + + @Test + void testPublicNotFoundPage() { + final ResponseEntity response = this.testRestTemplate.exchange("/public/notfound", HttpMethod.GET, + null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found"); + } + + @Test + void testPublicNotFoundPageWithCorrectCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") + .exchange("/public/notfound", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found"); + } + + @Test + void testPublicNotFoundPageWithBadCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrong") + .exchange("/public/notfound", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse).isNull(); + } + + @Test + void testCorrectCredentialsWithControllerException() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") + .exchange("/fail", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Internal Server Error"); + } + + @Test + void testCorrectCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") + .exchange("/test", HttpMethod.GET, null, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + response.getBody(); + assertThat(response.getBody()).isEqualTo("test"); + } + + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @RestController + static class TestController { + + @GetMapping("/test") + String test() { + return "test"; + } + + @GetMapping("/fail") + String fail() { + throw new RuntimeException(); + } + + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/ErrorPageTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/ErrorPageTests.java index 1ec30455ee..c33ef6deee 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/ErrorPageTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/ErrorPageTests.java @@ -16,21 +16,11 @@ package smoketest.web.secure; -import com.fasterxml.jackson.databind.JsonNode; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; /** * Tests to ensure that the error page is accessible only to authorized users. @@ -38,61 +28,24 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Madhura Bhave */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, - classes = { ErrorPageTests.TestConfiguration.class, SampleWebSecureApplication.class }, + classes = { AbstractErrorPageTests.TestConfiguration.class, ErrorPageTests.SecurityConfiguration.class, + SampleWebSecureApplication.class }, properties = { "server.error.include-message=always", "spring.security.user.name=username", "spring.security.user.password=password" }) -class ErrorPageTests { - - @Autowired - private TestRestTemplate testRestTemplate; - - @Test - void testBadCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrongpassword") - .exchange("/test", HttpMethod.GET, null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNull(); - } - - @Test - void testNoCredentials() { - final ResponseEntity response = this.testRestTemplate.exchange("/test", HttpMethod.GET, null, - JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNull(); - } - - @Test - void testPublicNotFoundPage() { - final ResponseEntity response = this.testRestTemplate.exchange("/public/notfound", HttpMethod.GET, - null, JsonNode.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - JsonNode jsonResponse = response.getBody(); - assertThat(jsonResponse).isNull(); - } - - @Test - void testCorrectCredentials() { - final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") - .exchange("/test", HttpMethod.GET, null, String.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - response.getBody(); - assertThat(response.getBody()).isEqualTo("test"); - } - - @Configuration(proxyBeanMethods = false) - static class TestConfiguration { - - @RestController - static class TestController { - - @GetMapping("/test") - String test() { - return "test"; - } - +class ErrorPageTests extends AbstractErrorPageTests { + + @org.springframework.boot.test.context.TestConfiguration(proxyBeanMethods = false) + static class SecurityConfiguration { + + @Bean + SecurityFilterChain configure(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> { + requests.antMatchers("/public/**").permitAll(); + requests.anyRequest().fullyAuthenticated(); + }); + http.httpBasic(); + http.formLogin((form) -> form.loginPage("/login").permitAll()); + return http.build(); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/NoSessionErrorPageTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/NoSessionErrorPageTests.java new file mode 100644 index 0000000000..9f98a4abcc --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/NoSessionErrorPageTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.web.secure; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +/** + * Tests for error page when a stateless session creation policy is used. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + classes = { AbstractErrorPageTests.TestConfiguration.class, NoSessionErrorPageTests.SecurityConfiguration.class, + SampleWebSecureApplication.class }, + properties = { "server.error.include-message=always", "spring.security.user.name=username", + "spring.security.user.password=password" }) +class NoSessionErrorPageTests extends AbstractErrorPageTests { + + @org.springframework.boot.test.context.TestConfiguration(proxyBeanMethods = false) + static class SecurityConfiguration { + + @Bean + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeRequests((requests) -> { + requests.antMatchers("/public/**").permitAll(); + requests.anyRequest().authenticated(); + }); + http.httpBasic(); + return http.build(); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java index a199557c58..d6c36befff 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java @@ -25,12 +25,15 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; 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.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -42,7 +45,8 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Dave Syer * @author Scott Frederick */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + classes = { SampleWebSecureApplicationTests.SecurityConfiguration.class, SampleWebSecureApplication.class }) class SampleWebSecureApplicationTests { @Autowired @@ -85,4 +89,21 @@ class SampleWebSecureApplicationTests { assertThat(entity.getHeaders().getLocation().toString()).endsWith(this.port + "/"); } + @org.springframework.boot.test.context.TestConfiguration(proxyBeanMethods = false) + static class SecurityConfiguration { + + @Bean + SecurityFilterChain configure(HttpSecurity http) throws Exception { + http.csrf().disable(); + http.authorizeRequests((requests) -> { + requests.antMatchers("/public/**").permitAll(); + requests.anyRequest().fullyAuthenticated(); + }); + http.httpBasic(); + http.formLogin((form) -> form.loginPage("/login").permitAll()); + return http.build(); + } + + } + } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/UnauthenticatedErrorPageTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/UnauthenticatedErrorPageTests.java new file mode 100644 index 0000000000..df7d92105a --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/UnauthenticatedErrorPageTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smoketest.web.secure; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for error page that permits access to all. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + classes = { AbstractErrorPageTests.TestConfiguration.class, + UnauthenticatedErrorPageTests.SecurityConfiguration.class, SampleWebSecureApplication.class }, + properties = { "server.error.include-message=always", "spring.security.user.name=username", + "spring.security.user.password=password" }) +class UnauthenticatedErrorPageTests { + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + void testBadCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrongpassword") + .exchange("/test", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Unauthorized"); + } + + @Test + void testNoCredentials() { + final ResponseEntity response = this.testRestTemplate.exchange("/test", HttpMethod.GET, null, + JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Unauthorized"); + } + + @Test + void testPublicNotFoundPage() { + final ResponseEntity response = this.testRestTemplate.exchange("/public/notfound", HttpMethod.GET, + null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found"); + } + + @Test + void testPublicNotFoundPageWithCorrectCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") + .exchange("/public/notfound", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found"); + } + + @Test + void testPublicNotFoundPageWithBadCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "wrong") + .exchange("/public/notfound", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Unauthorized"); + } + + @Test + void testCorrectCredentialsWithControllerException() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") + .exchange("/fail", HttpMethod.GET, null, JsonNode.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + JsonNode jsonResponse = response.getBody(); + assertThat(jsonResponse.get("error").asText()).isEqualTo("Internal Server Error"); + } + + @Test + void testCorrectCredentials() { + final ResponseEntity response = this.testRestTemplate.withBasicAuth("username", "password") + .exchange("/test", HttpMethod.GET, null, String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("test"); + } + + @org.springframework.boot.test.context.TestConfiguration(proxyBeanMethods = false) + static class SecurityConfiguration { + + @Bean + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> { + requests.antMatchers("/error").permitAll(); + requests.antMatchers("/public/**").permitAll(); + requests.anyRequest().authenticated(); + }); + http.httpBasic(); + return http.build(); + } + + } + +}