Closed
Description
There is an inconsistency in the default 401 Unauthorized response.
If client credentials are not provided:
- 401 Unauthorized response is returned
- The response includes JSON with the error (like with any exception exposed by /error endpoint)
If wrong client credentials are provided:
- 401 Unauthorized response is returned
- The response does not include JSON
Sample pom file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.eu.rubensa.springboot.error</groupId>
<artifactId>springboot-error-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-error-test</name>
<description>Project for testing Spring Boot error handling</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Sample test class:
package org.eu.rubensa.springboot.error;
import com.fasterxml.jackson.databind.JsonNode;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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;
/**
* The @SpringBootTest annotation will load the fully ApplicationContext. This
* will not use slicing and scan for all the stereotype annotations
* (@Component, @Service, @Respository and @Controller / @RestController) and
* loads the full application context.
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
// From Spring 2.3.0 "server.error.include-message" and
// "server.error.include-binding-errors" is set to "never"
properties = { "server.error.include-message=always",
/**
* When you add the Security starter without custom security configurations,
* Spring Boot endpoints will be secured using HTTP basic authentication with a
* default user and generated password. To override that, you can configure
* credentials in application.properties as follows
*/
"spring.security.user.name=username", "spring.security.user.password=password" })
public class BadCredentialsInconsistencyTest {
@Autowired
private TestRestTemplate testRestTemplate;
@Test
public void testBadCredentials() throws Exception {
final ResponseEntity<JsonNode> response = testRestTemplate.withBasicAuth("username", "wrongpassword")
.exchange("/test", HttpMethod.GET, null, JsonNode.class);
Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
JsonNode jsonResponse = response.getBody();
/**
* Since Spring Boot 2.0 the /error end-point is protected so the exception is
* not "exposed" as JSON.
* <p>
* see: https://github.com/spring-projects/spring-security/issues/4467
*/
Assertions.assertThat(jsonResponse).isNull();
}
@Test
public void testNoCredentials() throws Exception {
final ResponseEntity<JsonNode> response = testRestTemplate.exchange("/test", HttpMethod.GET, null, JsonNode.class);
Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
JsonNode jsonResponse = response.getBody();
/**
* Since Spring Boot 2.0 the /error end-point is protected so the exception is
* not "exposed" as JSON.
* <p>
* Curiously if no credentials provided the error page is reached to "expose"
* the exception as JSON.
* <p>
* see: https://github.com/spring-projects/spring-security/issues/4467
*/
Assertions.assertThat(jsonResponse.findValue("status").asInt()).isEqualTo(401);
Assertions.assertThat(jsonResponse.findValue("error").asText()).isEqualTo("Unauthorized");
Assertions.assertThat(jsonResponse.findValue("message").asText()).contains("Unauthorized");
Assertions.assertThat(jsonResponse.findValue("path").asText()).isEqualTo("/test");
}
/**
* A nested @Configuration class wild be used instead of the application’s
* primary configuration.
* <p>
* Unlike a nested @Configuration class, which would be used instead of your
* application’s primary configuration, a nested @TestConfiguration class is
* used in addition to your application’s primary configuration.
*/
@Configuration
/**
* Tells Spring Boot to start adding beans based on classpath settings, other
* beans, and various property settings.
*/
@EnableAutoConfiguration
/**
* The @ComponentScan tells Spring to look for other components, configurations,
* and services in the the TestWebConfig package, letting it find the
* TestController class.
* <p>
* We only want to test the classes defined inside this test configuration
*/
static class TestConfig {
@RestController
public static class TestController {
@GetMapping("/test")
public void doNothing() {
}
}
}
}
Spring Boot Version: 2.4.5
Related issues: