Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

StackOverflowException when adapter's AuthenticationManager gets published as a bean #10419

Closed
goto1134 opened this issue Oct 19, 2021 · 4 comments
Assignees
Labels
status: duplicate A duplicate of another issue type: bug A general bug

Comments

@goto1134
Copy link

goto1134 commented Oct 19, 2021

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

  2. Publish the security config authentication manager bean as shown below:

    @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()
         }
     }    
  3. Call any method with an invalid JWT token

  4. 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;
}
@goto1134 goto1134 added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Oct 19, 2021
@goto1134 goto1134 changed the title StackOverflowException when adapter's AuthenticationManager gets published as a bean StackOverflowException when adapter's AuthenticationManager gets published as a bean Oct 19, 2021
@sjohnr
Copy link
Member

sjohnr commented Oct 19, 2021

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.)

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Oct 19, 2021
@goto1134
Copy link
Author

goto1134 commented Oct 19, 2021

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.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Oct 19, 2021
@sjohnr
Copy link
Member

sjohnr commented Oct 20, 2021

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!

@sjohnr
Copy link
Member

sjohnr commented Oct 26, 2021

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.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
status: duplicate A duplicate of another issue type: bug A general bug
Projects
None yet
Development

No branches or pull requests

3 participants