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
pull/30003/head
Madhura Bhave 3 years ago
parent c077ebecf7
commit d9d161cd6b

@ -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.
*/

@ -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);

@ -66,7 +66,8 @@ class SampleActuatorCustomSecurityApplicationTests extends AbstractSampleActuato
void testInsecureApplicationPath() {
ResponseEntity<Map> entity = restTemplate().getForEntity(getPath() + "/foo", Map.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(entity.getBody()).isNull();
Map<String, Object> body = entity.getBody();
assertThat((String) body.get("message")).contains("Expected exception in controller");
}
@Test

@ -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();
}
}
}

@ -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<JsonNode> 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<JsonNode> 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<JsonNode> 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<JsonNode> 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<JsonNode> 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<JsonNode> 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<String> 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();
}
}
}
}

@ -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<JsonNode> 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<JsonNode> 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<JsonNode> 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<String> 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();
}
}

@ -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();
}
}
}

@ -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();
}
}
}

@ -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<JsonNode> 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<JsonNode> 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<JsonNode> 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<JsonNode> 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<JsonNode> 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<JsonNode> 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<String> 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();
}
}
}
Loading…
Cancel
Save