Response status code is incorrect when using multiple SecurityFilterChain(s).
It seems like the changes done in https://github.com/spring-projects/spring-boot/commit/cedd553b836d97a04d769322771bc1a8499e7282 for removing the ErrorPageSecurityFilter have lead to the use of multiple security filter chains not working correctly.
This might be linked to https://github.com/spring-projects/spring-security/issues/12771, and perhaps that is how Spring Security worked before. However, I do not agree that we need to add something additional to get the correct error code if we have configured it like that.
I have created a example repo with some tests and configuration.
The security configuration looks like:
@EnableWebSecurity
@Configuration
public class SecurityConfiguration {
@Bean
@Order(1)
public SecurityFilterChain firstSecurityFilterChain(HttpSecurity http) throws Exception {
return http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(AbstractHttpConfigurer::disable)
// Comment out line below for Spring Boot 2.7
//.antMatcher("/ignored-api/*")
// Comment line below for Spring Boot 2.7
.securityMatcher(AntPathRequestMatcher.antMatcher("/ignored-api/*"))
.authorizeHttpRequests(configurer -> configurer.anyRequest().denyAll())
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
@Order(2)
public SecurityFilterChain secondSecurityFilterChain(HttpSecurity http) throws Exception {
return http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exceptionHandling -> exceptionHandling.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
AnyRequestMatcher.INSTANCE))
.securityContext(securityContext -> securityContext.securityContextRepository(new NullSecurityContextRepository()))
.authorizeHttpRequests(configurer -> configurer.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.build();
}
}
My rest controller looks like:
@RestController
public class TestRestController {
@GetMapping("/ignored-api/forbidden")
public ResponseEntity<?> ignoredForbidden() {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null);
}
@GetMapping("/ignored-api/ok")
public ResponseEntity<?> ignoredOk() {
return ResponseEntity.status(HttpStatus.OK).body(null);
}
@GetMapping("/allowed-api/forbidden")
public ResponseEntity<?> allowedForbidden() {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null);
}
@GetMapping("/allowed-api/ok")
public ResponseEntity<?> allowedOk() {
return ResponseEntity.status(HttpStatus.OK).body(null);
}
}
and the tests look like:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestRestControllerTest {
@Autowired
protected TestRestTemplate restTemplate;
@Test
void ignoredForbiddenNotAuthenticated() {
ResponseEntity<String> response = restTemplate.getForEntity("/ignored-api/forbidden", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void ignoredForbiddenAuthenticated() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "test")
.getForEntity("/ignored-api/forbidden", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
void ignoredOkNotAuthenticated() {
ResponseEntity<String> response = restTemplate.getForEntity("/ignored-api/ok", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void ignoredOkAuthenticated() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "test")
.getForEntity("/ignored-api/ok", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
void ignoredUnknownNotAuthenticated() {
ResponseEntity<String> response = restTemplate.getForEntity("/ignored-api/dummy", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void ignoredUnknownAuthenticated() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "test")
.getForEntity("/ignored-api/dummy", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
void allowedForbiddenNotAuthenticated() {
ResponseEntity<String> response = restTemplate.getForEntity("/allowed-api/forbidden", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void allowedForbiddenAuthenticated() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "test")
.getForEntity("/allowed-api/forbidden", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
void allowedOkNotAuthenticated() {
ResponseEntity<String> response = restTemplate.getForEntity("/allowed-api/ok", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void allowedOkAuthenticated() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "test")
.getForEntity("/allowed-api/ok", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.OK);
}
@Test
void allowedUnknownNotAuthenticated() {
ResponseEntity<String> response = restTemplate.getForEntity("/allowed-api/dummy", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void allowedUnknownAuthenticated() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("user", "test")
.getForEntity("/allowed-api/dummy", String.class);
assertThat(response.getStatusCode()).as(response.toString()).isEqualTo(HttpStatus.NOT_FOUND);
}
}
In Spring Boot 2.7 all the tests are green. With Spring Boot 3.1 the following ones are failing:
ignoredUnknownAuthenticated- in 2.7 the status code is HTTP 403, with 3.1 it is HTTP 401ignoredForbiddenAuthenticated- in 2.7 the status code is HTTP 403, with 3.1 it is HTTP 401ignoredOkAuthenticated- in 2.7 the status code is HTTP 403, with 3.1 it is HTTP 401allowedUnknownAuthenticated- in 2.7 the status code is HTTP 404, with 3.1 it is HTTP 401
Comment From: philwebb
I'm afraid the comment I added to #33341 also applies here. We're really not keen to start deviating from Spring Security defaults. Please add a comment to https://github.com/spring-projects/spring-security/issues/12771 so that they have another data point for the problem.
Comment From: filiphr
Thanks for checking @philwebb. I'll add a comment to the Spring Security issue. It is slightly different than error pages, but it isn't much logical to me what Spring Security is doing.