Skip to content

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

Closed
@goto1134

Description

@goto1134

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;
}

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions