Security configuration class

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConf {

        @Bean
        public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
            return authenticationConfiguration.getAuthenticationManager();
        }
    }

Controller

    @RestController
    public class Controller {

        @GetMapping("/test")
        public String test(){
            return "test";
        }
    }

Test class

    @ExtendWith(SpringExtension.class)
    @WebMvcTest(controllers = Controller.class)
    @ComponentScan("com.example.demo")
    public class DemoApplicationTests {

        @Test
        public void test() {
        }
    }

I have a simple application, if I try to start the main it works, however when I start my test class I get this error:

java.lang.StackOverflowError
                at java.base/java.lang.StackTraceElement.of(StackTraceElement.java:526)
                at java.base/java.lang.Throwable.getOurStackTrace(Throwable.java:828)
                at java.base/java.lang.Throwable.getStackTrace(Throwable.java:820)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:79)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
                at ch.qos.logback.classic.spi.ThrowableProxy.<init>(ThrowableProxy.java:89)
        and it always continues the same way ...

In debug I tried to recover the error, but I only see this java.lang.IllegalStateException: Failed to unwrap proxied object.

The test only works if I remove the authenticationManagerBean, but i need authenticationManagerBean to inject AutthenticationManager in my app.

I know that I could use WebSecurityConfigurerAdapter and then add the bean going to override the method of this class. But as WebSecurityConfigurerAdapter is deprecated, I would not want to use it.

This problem arose from the fact that I have switched from WebSecurityConfigurerAdapter to SecurityFilterChain and am getting this error as I changed the config class.

I created this simple project which replicates the error of my main project. If you try to start the application it works, but the test doesn't. https://github.com/lako12/demo

The project is very simple, there are only 3 classes plus a test class.

Comment From: wilkinsona

Thanks for the sample. It was very helpful in diagnosing the problem.

I believe that the root cause is a bug in AopTestUtils.getUltimateTargetObject in Spring Framework. When it's called with the authenticationManager bean it recurses until the stack overflows. This behavior can be reproduced with the following minimal test:

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.util.AopTestUtils;

@SpringJUnitConfig
class MinimalTests {

    @Test
    void test(@Autowired AuthenticationManager authenticationManager) {
        AopTestUtils.getUltimateTargetObject(authenticationManager);
    }


    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    static class MinimalConfiguration {

        @Bean
        AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
            return authenticationConfiguration.getAuthenticationManager();
        }
    }

}

And these dependencies (using Spring Boot 2.7.4 purely for dependency management):

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

We'll transfer this issue to the Spring Framework team so that they can investigate.

Comment From: sbrannen

@lako12, thanks for bringing this to our attention!

@wilkinsona, thanks for analyzing it and providing the minimal reproducer!

It turns out that AopTestUtils.getUltimateTargetObject is indeed part of the issue, but... only when the bean passed to it is a Spring AOP proxy backed by a LazyInitTargetSource -- which is the case for Spring Security's AuthenticationManager (but only in certain scenarios resulting in AuthenticationConfiguration's lazyBean(AuthenticationManager.class) method being invoked).

If that scenario occurs, getUltimateTargetObject() ends up traversing an infinite object graph along the lines of: proxy -> Advised -> LazyInitTargetSource -> proxy ->Advised -> LazyInitTargetSource -> proxy -> ...

In light of that, my plan is to revise AopTestUtils.getUltimateTargetObject so that it returns the original object if the TargetSource is non-static (i.e., dynamic like LazyInitTargetSource). I've tested that locally; it resolves the issue; and I hope the change doesn't introduce adverse side effects for anyone intentionally invoking AopTestUtils.getUltimateTargetObject for other types of non-static target sources.

Comment From: marcusdacoregio

Hi everyone.

Despite the AOP bug, the Spring Security team does not recommend exposing the AuthenticationManager bean by using AuthenticationConfiguration#getAuthenticationManager. It can lead to weird behaviors like StackOverflowException. Oftentimes, folks just want to use their UserDetailsService and PasswordEncoder to authenticate the user, with that said, you can create the AuthenticationManager like so:

@Bean
AuthenticationManager authenticationManager(UserDetailsService myUserDetailsService, PasswordEncoder encoder) {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(myUserDetailsService);
    provider.setPasswordEncoder(encoder);
    return new ProviderManager(provider);
}

You can also rely on the AuthenticationFilter in order to create a custom authentication mechanism.

See: - https://github.com/spring-projects/spring-security/issues/11609 - https://github.com/spring-projects/spring-security/issues/10419 - https://github.com/spring-projects/spring-security/issues/10822#issuecomment-1141121007 - https://stackoverflow.com/a/73895991/5454842

Comment From: sbrannen

Team Decision

After further consideration, we have decided not to make any change to AopTestUtils.getUltimateTargetObject() with regard to a non-static TargetSource check.

AopTestUtils.getUltimateTargetObject() is not designed to be applied to every singleton bean within an ApplicationContext. Rather, the methods in AopTestUtils are designed for end users who wish to unwrap a proxy within the scope of an individual test where they know that the object is a proxied bean, and we would like to retain that focus.

However, it does make sense to highlight the limitations of getUltimateTargetObject() with regard to a non-static TargetSource, and we will add a note to the Javadoc to raise awareness.

Regarding the use of AopTestUtils.getUltimateTargetObject() in SpringBootMockResolver, we suggest introducing a more defensive variant of that code in Spring Boot. For example, something along the lines of the following may work well.

public static <T> T getUltimateTargetObject(Object candidate) {
    Assert.notNull(candidate, "Candidate must not be null");
    try {
        if (AopUtils.isAopProxy(candidate) && candidate instanceof Advised advised) {
            TargetSource targetSource = advised.getTargetSource();
            if (targetSource.isStatic()) {
                Object target = targetSource.getTarget();
                if (target != null) {
                    return (T) getUltimateTargetObject(target);
                }
            }
        }
    }
    catch (Throwable ex) {
        throw new IllegalStateException("Failed to unwrap proxied object", ex);
    }
    return (T) candidate;
}

In light of the above, we are closing this issue.

On the Boot side, I've created https://github.com/spring-projects/spring-boot/issues/32632 as a follow up.

On the Framework side, I've created #29276 to address the documentation enhancement.

Comment From: Rapter1990

Hi everyone.

Despite the AOP bug, the Spring Security team does not recommend exposing the AuthenticationManager bean by using AuthenticationConfiguration#getAuthenticationManager. It can lead to weird behaviors like StackOverflowException. Oftentimes, folks just want to use their UserDetailsService and PasswordEncoder to authenticate the user, with that said, you can create the AuthenticationManager like so:

java @Bean AuthenticationManager authenticationManager(UserDetailsService myUserDetailsService, PasswordEncoder encoder) { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(myUserDetailsService); provider.setPasswordEncoder(encoder); return new ProviderManager(provider); }

You can also rely on the AuthenticationFilter in order to create a custom authentication mechanism.

See:

@marcusdacoregio I tried to implement a spring boot microservice example. I have the same issue in my OrderServiceTest and OrderControllerTest in order service. I asked a question in stackoverflow. Here is the link : https://stackoverflow.com/questions/74633891/spring-boot-microservices-servicetest-and-controllertest-for-junit-throwing-ja How can I do that?

Comment From: marcusdacoregio

Hi @Rapter1990, please see this issue and contribute if necessary.