Skip to content

Spring Security AuthenticationException message inconsistency #26357

@rubensa

Description

@rubensa

In general, for any exception, the default behavior if you tell Spring Boot to include the error message via server.error.include-message=always is to include the Exception message in the JSON message attribute.
Looks like with any exception whose ancestor is org.springframework.security.core.AuthenticationException (like AccessDeniedException, AccountExpiredException, BadCredentialsException,...), the behavior differs and the message value contains the same value as error attribute.

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.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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 AuthenticationExceptionMessageInconsistencyTest {
  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  public void testExceptionMessage() throws Exception {
    String exceptionParam = "custom";

    final ResponseEntity<JsonNode> response = testRestTemplate.withBasicAuth("username", "password")
        .exchange("/exception/{exception_id}", HttpMethod.GET, null, JsonNode.class, exceptionParam);
    Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
    JsonNode jsonResponse = response.getBody();
    Assertions.assertThat(jsonResponse.findValue("status").asInt()).isEqualTo(500);
    Assertions.assertThat(jsonResponse.findValue("error").asText()).isEqualTo("Internal Server Error");
    // This is the exception message
    Assertions.assertThat(jsonResponse.findValue("message").asText()).isEqualTo("Custom exception");
    Assertions.assertThat(jsonResponse.findValue("path").asText()).isEqualTo("/exception/custom");
  }

  @Test
  public void testAuthenticationExceptionMessage() throws Exception {
    String exceptionParam = "custom-authentication";

    final ResponseEntity<JsonNode> response = testRestTemplate.withBasicAuth("username", "password")
        .exchange("/exception/{exception_id}", HttpMethod.GET, null, JsonNode.class, exceptionParam);
    Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    JsonNode jsonResponse = response.getBody();
    Assertions.assertThat(jsonResponse.findValue("status").asInt()).isEqualTo(401);
    Assertions.assertThat(jsonResponse.findValue("error").asText()).isEqualTo("Unauthorized");
    // This should be the exception message but is the same as error
    Assertions.assertThat(jsonResponse.findValue("message").asText()).isEqualTo("Unauthorized");
    Assertions.assertThat(jsonResponse.findValue("path").asText()).isEqualTo("/exception/custom-authentication");
  }

  /**
   * 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 class TestController {
      @GetMapping("/exception/{exception_id}")
      public void getSpecificException(@PathVariable("exception_id") String pException) {
        if ("custom".equals(pException)) {
          throw new CustomException("Custom exception");
        } else if ("custom-authentication".equals(pException)) {
          throw new MyAuthenticationException("Custom authentication exception");
        }
      }
    }

    public class CustomException extends RuntimeException {
      public CustomException(String message) {
        super(message);
      }
    }

    public class MyAuthenticationException extends AuthenticationException {
      public MyAuthenticationException(String message) {
        super(message);
      }
    }
  }
}

Spring Boot Version: 2.4.5

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: supersededAn issue that has been superseded by another

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions