Do not enable WebFlux security unless other configuration is active

Following the changes in gh-37504, the reactive resource server
auto-configuration could enable WebFlux security in situations where
it was otherwise in active. This could then result in an application
failing to start as no authentication manager is available.

This commit updates the configurations that enable WebFlux security
so that they fully back off unless their related configurations are
active. Previously, only the configuration of the
SecurityWebFilterChain would back off. This has been expanded to
cover `@EnableWebFluxSecurity` as well. This has required splitting
the configuration classes up so that the condition evaluation order
can be controlled more precisely. We need to ensure that the JWT
decoder bean or the opaque token introspector bean has been defined
before evaluation of the conditions for `@EnableWebFluxSecurity`.
Without this control, the import through `@EnableWebFluxSecurity` in
one location where the conditions do not matchcan prevent a
successful import in another where they do.

Fixes gh-38713
This commit is contained in:
Andy Wilkinson 2023-12-11 10:45:15 +00:00
parent 6330190913
commit beba1f176a
5 changed files with 35 additions and 12 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -40,7 +40,9 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
@ConditionalOnClass({ EnableWebFluxSecurity.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@Import({ ReactiveOAuth2ResourceServerConfiguration.JwtConfiguration.class,
ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class })
ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class,
ReactiveOAuth2ResourceServerConfiguration.JwtWebSecurityConfiguration.class,
ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenWebSecurityConfiguration.class })
public class ReactiveOAuth2ResourceServerAutoConfiguration {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -24,8 +24,8 @@ import org.springframework.security.oauth2.server.resource.authentication.Bearer
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
/**
* Configuration classes for OAuth2 Resource Server These should be {@code @Import} in a
* regular auto-configuration class to guarantee their order of execution.
* Configuration classes for OAuth2 Resource Server. These should be {@code @Import}ed in
* a regular auto-configuration class to guarantee their order of execution.
*
* @author Madhura Bhave
*/
@ -33,18 +33,30 @@ class ReactiveOAuth2ResourceServerConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class,
ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class })
@Import(ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class)
static class JwtConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
@Import(ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class)
static class JwtWebSecurityConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class })
@Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class,
ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class })
@Import(ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class)
static class OpaqueTokenConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class })
@Import(ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class)
static class OpaqueTokenWebSecurityConfiguration {
}
}

View File

@ -164,11 +164,11 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ReactiveJwtDecoder.class)
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
static class WebSecurityConfiguration {
@Bean
@ConditionalOnBean(ReactiveJwtDecoder.class)
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated());
http.oauth2ResourceServer((server) -> customDecoder(server, jwtDecoder));

View File

@ -56,10 +56,10 @@ class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
@ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class)
static class WebSecurityConfiguration {
@Bean
@ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class)
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated());
http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults()));

View File

@ -43,6 +43,8 @@ import org.mockito.InOrder;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
@ -73,6 +75,7 @@ import org.springframework.security.oauth2.server.resource.authentication.Opaque
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.WebFilter;
@ -116,10 +119,16 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
}
}
@Test
void autoConfigurationDoesNotEnableWebSecurityWithoutJwtDecoderOrTokenIntrospector() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(WebFilterChainProxy.class));
}
@Test
void autoConfigurationShouldConfigureResourceServer() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run((context) -> {
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
@ -385,7 +394,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
@Test
void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() {
this.contextRunner
this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",