Describe the bug

When multiple ReactiveAuthenticationManager beans are defined in a WebFlux application (for example, one for opaque token introspection and another for basic authentication), the application fails to start.

Spring Security’s internal configuration (ServerHttpSecurityConfiguration) attempts to autowire a single ReactiveAuthenticationManager without considering qualifiers, which leads to an ambiguity error.

This happens even if the filter chains explicitly reference the correct managers using qualifiers in the DSL.

This seems very similar to issue #9256, where the same problem occurred in HttpSecurityConfiguration (the servlet-based configuration). In this case, the failure happens in ServerHttpSecurityConfiguration, which appears to be the reactive counterpart of HttpSecurityConfiguration.

Description:

Parameter 0 of method setAuthenticationManager in org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfiguration required a single bean, but 2 were found:
        - basicAuthenticationManager: defined by method 'basicAuthenticationManager' in class path resource [.../configuration/SecurityConfiguration.class]
        - opaqueAuthManager: defined by method 'opaqueAuthManager' in class path resource [.../configuration/SecurityConfiguration.class]

This may be due to missing parameter name information


Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

At the moment, the only way I managed to get the application to start was by adding a fallback Primary ReactiveAuthenticationManager that simply rejects all authentication attempts. This ensures the context initializes correctly while the actual filter chains still use their explicitly configured managers.

It works, but it feels a bit hacky and not what I expected when using qualifiers

    @Bean
    @Primary
    fun fallbackReactiveAuthenticationManager(): ReactiveAuthenticationManager {
        return ReactiveAuthenticationManager { _ ->
            Mono.error(BadCredentialsException("No qualified ReactiveAuthenticationManager available"))
        }
    }

To Reproduce

  1. Create a new Spring Boot WebFlux application (we are currently using version 3.5.6) with Spring Security.
  2. Add two ReactiveAuthenticationManager beans (for example, one for opaque token introspection and one for basic authentication).
  3. Configure a SecurityWebFilterChain that explicitly wires these managers using qualifiers in the DSL.
  4. Start the application.

Expected behavior Since I’m explicitly qualifying the managers inside my Configuration Class, Spring Security should be fine with both managers coexisting.

Sample

@Configuration
@EnableWebFluxSecurity
class SecurityConfiguration {

    @Bean
    @Qualifier("opaqueAuthManager")
    fun opaqueAuthManager(introspector: ReactiveOpaqueTokenIntrospector): OpaqueTokenReactiveAuthenticationManager =
        OpaqueTokenReactiveAuthenticationManager(introspector)

    @Bean
    @Qualifier("basicAuthManager")
    fun basicAuthManager(securityProperties: SecurityProperties): ReactiveAuthenticationManager {
        val user: SecurityProperties.User = securityProperties.user
        val userDetails = User.withUsername(user.name).password(user.password).build()
        return UserDetailsRepositoryReactiveAuthenticationManager(MapReactiveUserDetailsService(userDetails))
    }

    @Bean
    fun apiSecurity(http: ServerHttpSecurity,
    ): SecurityWebFilterChain {
        return http {
            securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/**"))
            authorizeExchange {
                authorize(anyExchange, authenticated)
            }
            oauth2ResourceServer { opaqueToken { } }
            httpBasic { }
            formLogin { disable() }
            csrf { disable() }
            cors { disable() }
            logout { disable() }
            exceptionHandling { authenticationEntryPoint = HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED) }
            authenticationManager = basicAuthManager
        }
    }
}