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 838a593f2de..3a5b0c6ba2c 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 @@ -30,6 +30,7 @@ import jakarta.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; @@ -62,17 +63,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() { @@ -93,11 +105,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 cdd1327bc23..04fd7f02fba 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 jakarta.servlet.DispatcherType; import jakarta.servlet.FilterChain; import jakarta.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 27fa0703a5f..841e9c96463 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 b74d0337a62..e60992adbdb 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 00000000000..2affea3b3ba --- /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 1ec30455eeb..c33ef6deee6 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 00000000000..9f98a4abcc1 --- /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 a199557c58b..d6c36befff1 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 00000000000..df7d92105ac --- /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(); + } + + } + +}