problem-spring-web: IllegalStateException: Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.

Description

The ProblemSecurityAutoConfiguration registers a WebSecurityConfigurerAdapter and causes this issue when combined with the spring-boot-security-starter causes https://github.com/spring-projects/spring-security/issues/9295.

Expected Behavior

Exception above.

Actual Behavior

Register configurer if and only if we don’t interfere with the default.

Possible Fix

Steps to Reproduce

Context

Your Environment

  • Version used:
  • Link to your project:

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 2
  • Comments: 15 (1 by maintainers)

Most upvoted comments

This is how I managed to get a working test but I had to manually configure the Problem Security support (so I don’t like the solution at all): SpringSecurityExceptionTest.java

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.autoconfigure.condition.ConditionalOnMissingBean;
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.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.zalando.problem.spring.web.advice.AdviceTrait;
import org.zalando.problem.spring.web.advice.ProblemHandling;
import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait;
import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport;
import org.zalando.problem.spring.web.autoconfigure.security.ProblemSecurityAutoConfiguration;

/**
 * The {@link org.springframework.boot.test.context.SpringBootTest} annotation
 * will load the fully ApplicationContext. This will not use slicing and scan
 * for all the stereotype annotations
 * ({@link org.springframework.stereotype.Component},
 * {@link org.springframework.stereotype.Service},
 * {@link org.springframework.stereotype.Repository} and
 * {@link org.springframework.stereotype.Controller} /
 * {@link org.springframework.web.bind.annotation.RestController}) and loads the
 * full application context.
 */
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
    /**
     * 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 SpringSecurityExceptionTest {
  private static final String USER_NAME = "username";
  private static final String USER_PASSWORD = "password";

  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  public void testAccessOK() throws Exception {
    final ResponseEntity<String> response = testRestTemplate.withBasicAuth(USER_NAME, USER_PASSWORD)
        .exchange("/access-ok", HttpMethod.GET, null, String.class);
    Assertions.assertThat(response.getBody()).isEqualTo("OK");
  }

  @Test
  public void testAccessDenied() throws Exception {
    final ResponseEntity<JsonNode> response = testRestTemplate.withBasicAuth(USER_NAME, USER_PASSWORD)
        .exchange("/deny-all", HttpMethod.GET, null, JsonNode.class);
    Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    JsonNode jsonResponse = response.getBody();
    Assertions.assertThat(jsonResponse.findValue("status").asInt()).isEqualTo(403);
    Assertions.assertThat(jsonResponse.findValue("title").asText()).isEqualTo("Forbidden");
  }

  /**
   * A single {@link org.springframework.boot.autoconfigure.SpringBootApplication}
   * annotation can be used to enable:
   * <ul>
   * <li>{@link org.springframework.boot.autoconfigure.EnableAutoConfiguration}:
   * enable Spring Boot’s auto-configuration mechanism</li>
   * <li>{@link org.springframework.context.annotation.ComponentScan}: enable
   * {@link org.springframework.stereotype.Component} scan on the package where
   * the application is located (see the best practices)</li>
   * <li>{@link org.springframework.context.annotation.Configuration}: allow to
   * register extra beans in the context or import additional configuration
   * classes</li>
   * </ul>
   * <p>
   * A nested {@link org.springframework.context.annotation.Configuration} class
   * wild be used instead of the application’s primary configuration.
   * <p>
   * Unlike a nested {@link org.springframework.context.annotation.Configuration}
   * class, which would be used instead of your application’s primary
   * configuration, a nested
   * {@link org.springframework.boot.test.context.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.
   * <p>
   * Exclude {@link ProblemSecurityAutoConfiguration}. see:
   * https://github.com/zalando/problem-spring-web/issues/573
   */
  @EnableAutoConfiguration(exclude = ProblemSecurityAutoConfiguration.class)
  /**
   * Provides AOP security on methods. Some of the annotations that it provides
   * are PreAuthorize, PostAuthoriz
   */
  @EnableGlobalMethodSecurity(prePostEnabled = true)
  /**
   * The {@link org.springframework.context.annotation.ComponentScan} tells Spring
   * to look for other components, configurations, and services in the the
   * TestConfig package.
   * <p>
   * We only want to test the classes defined inside this test configuration so we
   * do nos use it here.
   */
  static class TestConfig {
    @Configuration
    @Import(SecurityProblemSupport.class)
    public class ProblemSecurityConfiguration extends WebSecurityConfigurerAdapter {
      private final SecurityProblemSupport support;

      public ProblemSecurityConfiguration(SecurityProblemSupport support) {
        this.support = support;
      }

      @Override
      public void configure(final HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic().and()
            .exceptionHandling().authenticationEntryPoint(support).accessDeniedHandler(support);
      }

      @Bean
      public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
        return new DefaultAuthenticationEventPublisher(publisher);
      }

      @Bean
      @ConditionalOnMissingBean(AdviceTrait.class)
      public AdviceTrait securityExceptionHandling() {
        return new SecurityExceptionHandling();
      }
    }

    @ControllerAdvice
    final class SecurityExceptionHandling implements ProblemHandling, SecurityAdviceTrait {
    }

    @RestController
    public static class TestController {
      @GetMapping("/access-ok")
      public @ResponseBody String getAccessOk() {
        return "OK";
      }

      @GetMapping("/deny-all")
      @PreAuthorize("denyAll()")
      public void getAccessDenied() {
      }
    }
  }
}

Sample test to show the problem:

pom.xml

<?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>

SpringSecurityExceptionTest.java

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.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * The {@link org.springframework.boot.test.context.SpringBootTest} annotation
 * will load the fully ApplicationContext. This will not use slicing and scan
 * for all the stereotype annotations
 * ({@link org.springframework.stereotype.Component},
 * {@link org.springframework.stereotype.Service},
 * {@link org.springframework.stereotype.Repository} and
 * {@link org.springframework.stereotype.Controller} /
 * {@link org.springframework.web.bind.annotation.RestController}) and loads the
 * full application context.
 */
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
    /**
     * 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 SpringSecurityExceptionTest {
  private static final String USER_NAME = "username";
  private static final String USER_PASSWORD = "password";

  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  public void testAccessOK() throws Exception {
    final ResponseEntity<String> response = testRestTemplate.withBasicAuth(USER_NAME, USER_PASSWORD)
        .exchange("/access-ok", HttpMethod.GET, null, String.class);
    Assertions.assertThat(response.getBody()).isEqualTo("OK");
  }

  @Test
  public void testAccessDenied() throws Exception {
    final ResponseEntity<JsonNode> response = testRestTemplate.withBasicAuth(USER_NAME, USER_PASSWORD)
        .exchange("/deny-all", HttpMethod.GET, null, JsonNode.class);
    Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    JsonNode jsonResponse = response.getBody();
    Assertions.assertThat(jsonResponse.findValue("status").asInt()).isEqualTo(403);
    Assertions.assertThat(jsonResponse.findValue("error").asText()).isEqualTo("Forbidden");
    Assertions.assertThat(jsonResponse.findValue("path").asText()).isEqualTo("/deny-all");
  }

  /**
   * A single {@link org.springframework.boot.autoconfigure.SpringBootApplication}
   * annotation can be used to enable:
   * <ul>
   * <li>{@link org.springframework.boot.autoconfigure.EnableAutoConfiguration}:
   * enable Spring Boot’s auto-configuration mechanism</li>
   * <li>{@link org.springframework.context.annotation.ComponentScan}: enable
   * {@link org.springframework.stereotype.Component} scan on the package where
   * the application is located (see the best practices)</li>
   * <li>{@link org.springframework.context.annotation.Configuration}: allow to
   * register extra beans in the context or import additional configuration
   * classes</li>
   * </ul>
   * <p>
   * A nested {@link org.springframework.context.annotation.Configuration} class
   * wild be used instead of the application’s primary configuration.
   * <p>
   * Unlike a nested {@link org.springframework.context.annotation.Configuration}
   * class, which would be used instead of your application’s primary
   * configuration, a nested
   * {@link org.springframework.boot.test.context.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
  /**
   * Provides AOP security on methods. Some of the annotations that it provides
   * are PreAuthorize, PostAuthoriz
   */
  @EnableGlobalMethodSecurity(prePostEnabled = true)
  /**
   * The {@link org.springframework.context.annotation.ComponentScan} tells Spring
   * to look for other components, configurations, and services in the the
   * TestConfig package.
   * <p>
   * We only want to test the classes defined inside this test configuration so we
   * do nos use it here.
   */
  static class TestConfig {
    @RestController
    public static class TestController {
      @GetMapping("/access-ok")
      public @ResponseBody String getAccessOk() {
        return "OK";
      }

      @GetMapping("/deny-all")
      @PreAuthorize("denyAll()")
      public void getAccessDenied() {
      }
    }
  }
}

The tests pass.

If I add problem-spring-web:

pom.xml

<?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>
    <problem-spring-web.version>0.26.2</problem-spring-web.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.zalando</groupId>
      <artifactId>problem-spring-web-starter</artifactId>
      <version>${problem-spring-web.version}</version>
      <type>pom</type>
    </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>

Same SpringSecurityExceptionTest.java as before.

The Spring context can’t be build.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityFilterChain' defined in class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.servlet.Filter]: Factory method 'springSecurityFilterChain' threw exception; nested exception is java.lang.IllegalStateException: Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.

If I exclude the ProblemSecurityAutoConfiguration and adapt the expected json respone check for the testAccessDenied():

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.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.zalando.problem.spring.web.autoconfigure.security.ProblemSecurityAutoConfiguration;

/**
 * The {@link org.springframework.boot.test.context.SpringBootTest} annotation
 * will load the fully ApplicationContext. This will not use slicing and scan
 * for all the stereotype annotations
 * ({@link org.springframework.stereotype.Component},
 * {@link org.springframework.stereotype.Service},
 * {@link org.springframework.stereotype.Repository} and
 * {@link org.springframework.stereotype.Controller} /
 * {@link org.springframework.web.bind.annotation.RestController}) and loads the
 * full application context.
 */
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
    /**
     * 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 SpringSecurityExceptionTest {
  private static final String USER_NAME = "username";
  private static final String USER_PASSWORD = "password";

  @Autowired
  private TestRestTemplate testRestTemplate;

  @Test
  public void testAccessOK() throws Exception {
    final ResponseEntity<String> response = testRestTemplate.withBasicAuth(USER_NAME, USER_PASSWORD)
        .exchange("/access-ok", HttpMethod.GET, null, String.class);
    Assertions.assertThat(response.getBody()).isEqualTo("OK");
  }

  @Test
  public void testAccessDenied() throws Exception {
    final ResponseEntity<JsonNode> response = testRestTemplate.withBasicAuth(USER_NAME, USER_PASSWORD)
        .exchange("/deny-all", HttpMethod.GET, null, JsonNode.class);
    Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    JsonNode jsonResponse = response.getBody();
    Assertions.assertThat(jsonResponse.findValue("status").asInt()).isEqualTo(403);
    Assertions.assertThat(jsonResponse.findValue("title").asText()).isEqualTo("Forbidden");
  }

  /**
   * A single {@link org.springframework.boot.autoconfigure.SpringBootApplication}
   * annotation can be used to enable:
   * <ul>
   * <li>{@link org.springframework.boot.autoconfigure.EnableAutoConfiguration}:
   * enable Spring Boot’s auto-configuration mechanism</li>
   * <li>{@link org.springframework.context.annotation.ComponentScan}: enable
   * {@link org.springframework.stereotype.Component} scan on the package where
   * the application is located (see the best practices)</li>
   * <li>{@link org.springframework.context.annotation.Configuration}: allow to
   * register extra beans in the context or import additional configuration
   * classes</li>
   * </ul>
   * <p>
   * A nested {@link org.springframework.context.annotation.Configuration} class
   * wild be used instead of the application’s primary configuration.
   * <p>
   * Unlike a nested {@link org.springframework.context.annotation.Configuration}
   * class, which would be used instead of your application’s primary
   * configuration, a nested
   * {@link org.springframework.boot.test.context.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.
   * <p>
   * Eclude {@link ProblemSecurityAutoConfiguration}. see:
   * https://github.com/zalando/problem-spring-web/issues/573
   */
  @EnableAutoConfiguration(exclude = ProblemSecurityAutoConfiguration.class)
  /**
   * Provides AOP security on methods. Some of the annotations that it provides
   * are PreAuthorize, PostAuthoriz
   */
  @EnableGlobalMethodSecurity(prePostEnabled = true)
  /**
   * The {@link org.springframework.context.annotation.ComponentScan} tells Spring
   * to look for other components, configurations, and services in the the
   * TestConfig package.
   * <p>
   * We only want to test the classes defined inside this test configuration so we
   * do nos use it here.
   */
  static class TestConfig {
    @RestController
    public static class TestController {
      @GetMapping("/access-ok")
      public @ResponseBody String getAccessOk() {
        return "OK";
      }

      @GetMapping("/deny-all")
      @PreAuthorize("denyAll()")
      public void getAccessDenied() {
      }
    }
  }
}

The testAccessDenied() do not pass as an error 500 is issued (instead of the expected 403).

[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 5.272 s <<< FAILURE! - in org.eu.rubensa.springboot.error.SpringSecurityExceptionTest
[ERROR] testAccessDenied  Time elapsed: 1.019 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: 

Expecting:
 <500 INTERNAL_SERVER_ERROR>
to be equal to:
 <403 FORBIDDEN>
but was not.
        at org.eu.rubensa.springboot.error.SpringSecurityExceptionTest.testAccessDenied(SpringSecurityExceptionTest.java:60)

2021-05-11 13:33:58.190  INFO 18631 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

You need to exclude the problem security auto configuration. On Fri, 12 Feb 2021, 11:08 Tim, @.***> wrote: Is there a workaround to make Spring 2.4 work? — You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub <#573 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADI7HM6WMPPRNYCCT5DKZDS6T43RANCNFSM4VGZU5KA .

@Monax111 you can disable ProblemSecurityAutoConfiguration by:

@EnableAutoConfiguration(exclude = ProblemSecurityAutoConfiguration.class)

But the problem then, @whiskeysierra, is that if a security exception is thrown the org.zalando.problem.spring.web.autoconfigure.ExceptionHandling#handlleThrowable(Throwable, NativeWebRequest) is invoked and produces a 500 INTERNAL_SERVER_ERROR (as the Problem implementation for Spring Security Exceptions has not been configured)

@whiskeysierra any idea on how to configure the Problem support for Spring Security but not getting the IllegalStateException: Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one?

Looks like org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration (imported from org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration) is the culprit of creating the SecurityFilterChain.