Description
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
-
Use
org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.5.3
-
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() } }
-
Call any method with an invalid JWT token
-
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;
}