Describe the bug If you publish an AuthenticationManager with org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#authenticationManagerBean, you will get a StackOverflowException.

To Reproduce 1. Use org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.5.3 1. Publish the security config authentication manager bean as shown below: ```kotlin @Configuration class SecurityConfig() : WebSecurityConfigurerAdapter() {

    private fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("groups")
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("")
        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter)
        return jwtAuthenticationConverter
    }


    override fun configure(http: HttpSecurity) {
        http.csrf().disable()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .oauth2ResourceServer().jwt()
            .jwtAuthenticationConverter(jwtAuthenticationConverter())
    }

    @Bean
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }
}    
```
  1. Call any method with an invalid JWT token
  2. Get StackOverflowException with the following calls: at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:510) at jdk.internal.reflect.GeneratedMethodAccessor96.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.base/java.lang.reflect.Method.invoke(Unknown Source) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208) at com.sun.proxy.$Proxy147.authenticate(Unknown Source) at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:201) at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:510)

Expected behaviour Get an authentication error without the stack overflow.

Sample None, sorry

Why does it happen WebSecurityConfigurerAdapter configures the published AuthenticationManager bean as a parent for the AuthenticationManagerBuilder. The builder then creates the ProviderManager, which will have our AuthenticationManager bean as a parent. This configuration creates a circular dependency.

If all of the configured AuthenticationProviders fail to authenticate, the ProviderManager will call its parent's authenticate method. The bean will call the ProviderManager again and so on. The following code is taken from the ProviderManager class to illustrate the algorithm:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    ...
    Authentication result = null;
    ...
    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }
            ...
        try {
                    // the JWT provider creates an `AuthenticationException` due to the invalid JWT token
            result = provider.authenticate(authentication);
            ...
        }
        catch (AccountStatusException | InternalAuthenticationServiceException ex) {
            ...
            throw ex;
        }
        catch (AuthenticationException ex) {
                    // the exception is ignored to try another provider
            lastException = ex;
        }
    }
    // as we know, the parent is not null and the result is null
    if (result == null && this.parent != null) {
       try {
          // this is the point of the recursive call
          parentResult = this.parent.authenticate(authentication);
        }
   ...
}

An ugly way to make it work 1

Add this line to your configure method: http.getSharedObject(AuthenticationManagerBuilder::class.java).parentAuthenticationManager(null).

All together:

@Configuration
class SecurityConfig() : WebSecurityConfigurerAdapter() {

    private fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("groups")
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("")
        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter)
        return jwtAuthenticationConverter
    }


    override fun configure(http: HttpSecurity) {
        http.getSharedObject(AuthenticationManagerBuilder::class.java).parentAuthenticationManager(null)
        http.csrf().disable()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .oauth2ResourceServer().jwt()
            .jwtAuthenticationConverter(jwtAuthenticationConverter())
    }

    @Bean
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }
}    

An ugly way to make it work 2*

Оverride this method in your SecurityConfig:

override fun authenticationManager(): AuthenticationManager? {
    return null;
}

Comment From: sjohnr

Hi @goto1134, thanks for the bug report. I have a few questions that might help clarify the issue in your case:

  1. What type of AuthenticationManager are you exposing as a bean? Is it customized or simply the one built by the base class, as in your example?
  2. What does the application do with the exposed AuthenticationManager? Are you trying to build a custom authentication endpoint, for example?
  3. Have you tried to give the bean a custom name, as in @Bean(name = "myAuthenticationManager")? (This is suggested in the javadoc for the authenticationManagerBean() method.)

Comment From: goto1134

What type of AuthenticationManager are you exposing as a bean? Is it customized or simply the one built by the base class, as in your example?

  1. I expose the bean built by the base class.

What does the application do with the exposed AuthenticationManager? Are you trying to build a custom authentication endpoint, for example?

  1. I need o expose AuthenticationManager to use it with grpc-spring-boot-starter https://yidongnan.github.io/grpc-spring-boot-starter/en/server/security.html#authentication-and-authorization. My app has both HTTP and gRPC APIs.

Have you tried to give the bean a custom name, as in @Bean(name = "myAuthenticationManager")? (This is suggested in the javadoc for the authenticationManagerBean() method.)

  1. Yes, I tried it, but since the logic of WebSecurityConfigurerAdapter#getHttp and WebSecurityConfigurerAdapter#authenticationManager does not depend on the bean name, it has no effect on the behaviour. The same example above can be used with a named bean to reproduce the problem.

Comment From: sjohnr

Thanks for the feedback, @goto1134. That's very helpful context. I was able to put together a sample based on your provided snippet, and reproduced the problem you're having. Ultimately, we should be able to inject the AuthenticationManager provided by the framework, but it doesn't seem to be possible currently.

However, you may consider a workaround in the meantime, which is to expose your own JwtAuthenticationProvider bean:

    @Bean
    fun jwtAuthenticationProvider(jwtDecoder: JwtDecoder, jwtAuthenticationConverter: JwtAuthenticationConverter): JwtAuthenticationProvider {
        val jwtAuthenticationProvider = JwtAuthenticationProvider(jwtDecoder)
        jwtAuthenticationProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter)
        return jwtAuthenticationProvider
    }

This bean gets used by the configurer from the DSL. It can also be injected wherever you would have used the AuthenticationManager. If you prefer to use an AuthenticationManager directly instead, you can add:

    @Bean
    fun authenticationManager(jwtAuthenticationProvider: JwtAuthenticationProvider): AuthenticationManager {
        val anonymousAuthenticationProvider = AnonymousAuthenticationProvider(UUID.randomUUID().toString())
        return ProviderManager(anonymousAuthenticationProvider, jwtAuthenticationProvider)
    }

This is essentially what the framework will build and pass into the BearerTokenAuthenticationFilter on your behalf. Check out this gist for the full configuration and (if you're interested) how to use the Kotlin DSL!

Comment From: sjohnr

Upon further investigation, this issue appears to be a duplicate of #8369, though the stack trace is slightly different. In fact, it's possible the stack trace will be different most of the time due to the recursive AuthenticationManager#authenticate() call acting on the request as if it was the first time it was processed in each level of the invocation and entering many additional levels of the stack and then backtracking on a failed authentication.

@goto1134, let me know if you have any questions with the above workaround. I'm going to close this as a duplicate.