From afad358047c4fa9a0a9281d406e875279528a495 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Dec 2023 12:44:04 +0000 Subject: [PATCH] Align reactive web security more closely with servlet web security There are some notable differences in the behavior of Spring Security's reactive and servlet-based web security. Notably, Servlet-based web security (`@EnableWebSecurity`) works without any authentication manager, rejecting requests as not authorized. By contrast reactive-based web security (`@EnableWebFluxSecurity`) fails to start up when there's no authentication manager, either provided directly as a bean or derived from a ReactiveUserDetailsService. There are also further differences at runtime where empty Monos from all ReactiveAuthenticationManagers results in an internal error and a 500 response whereas a similar situation in the servlet implementation results in a 401. Previously, to accommodate these differences in behavior, Spring Boot's auto-configuration would behave differently. In the Servlet case, web security would be enabled whenever the necessary dependencies were on the classpath. In the reactive case, web security would back off in the absence of an authentication manager to prevent a start up failure. While this difference is rooted in Spring Security, it is undesirable and something that we want to avoid Spring Boot users being exposed to where possible. Unfortunately, the situation is more likely to occur than before as ReactiveUserDetailsServiceAutoConfiguration now backs off more readily (gh-35338). This makes it more likely that the context will contain neither a reactive authetication manager not a reactive user details service. This commit reworks the auto-configurations related to reactive security. ReactiveSecurityAutoConfiguration will now auto-configure an "empty" reactive authentication manager that denies access through Mono.error in the absence of a ReactiveAuthenticationManager, ReactiveUserDetailsService, or SecurityWebFilterChain. The last of these is to allow for the situation where a filter chain has been defined with an authentication manager configured directly on it. This configuration of an authentication manager allows `@EnableWebFluxSecurity` to be auto-configured more readily, removing one of the differences between reactive- and Servlet-based security. Corresponding updates to the auto-configurations for reactive OAuth2 support have also been made. They no longer try to auto-configure `@EnableWebFluxSecurity`, relying instead upon ReactiveSecurityAutoConfiguration, which they are ordered before, to do that instead. Closes gh-38713 --- .../ReactiveOAuth2ClientConfigurations.java | 9 ----- ...eOAuth2ResourceServerJwkConfiguration.java | 9 ----- ...esourceServerOpaqueTokenConfiguration.java | 9 ----- .../ReactiveSecurityAutoConfiguration.java | 40 +++++++------------ ...2ResourceServerAutoConfigurationTests.java | 2 +- ...eactiveSecurityAutoConfigurationTests.java | 21 ++++++---- 6 files changed, 29 insertions(+), 61 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java index 8d25fbc2e34..8eb3871b937 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java @@ -28,7 +28,6 @@ import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2Clien import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; @@ -38,7 +37,6 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.WebFilterChainProxy; import static org.springframework.security.config.Customizer.withDefaults; @@ -94,13 +92,6 @@ class ReactiveOAuth2ClientConfigurations { return http.build(); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 31cb13aa60c..5f5cba160ea 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -35,7 +35,6 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2Res import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; @@ -50,7 +49,6 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUr import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.util.CollectionUtils; /** @@ -179,13 +177,6 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { server.jwt((jwt) -> jwt.jwtDecoder(decoder)); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java index dbeb778d876..f4d9614253e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java @@ -22,12 +22,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.WebFilterChainProxy; import static org.springframework.security.config.Customizer.withDefaults; @@ -66,13 +64,6 @@ class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration { return http.build(); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java index 5bcb151be46..9e07d8f9b98 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java @@ -17,21 +17,21 @@ package org.springframework.boot.autoconfigure.security.reactive; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -52,33 +52,21 @@ import org.springframework.web.reactive.config.WebFluxConfigurer; @ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, WebFilterChainProxy.class, WebFluxConfigurer.class }) public class ReactiveSecurityAutoConfiguration { - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) - @Conditional(EnableWebFluxSecurityCondition.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - - static final class EnableWebFluxSecurityCondition extends AnyNestedCondition { - - EnableWebFluxSecurityCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnBean(ReactiveAuthenticationManager.class) - static final class ConditionalOnReactiveAuthenticationManagerBean { - - } - - @ConditionalOnBean(ReactiveUserDetailsService.class) - static final class ConditionalOnReactiveUserDetailsService { + @Configuration(proxyBeanMethods = false) + class SpringBootWebFluxSecurityConfiguration { + @Bean + @ConditionalOnMissingBean({ ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class, + SecurityWebFilterChain.class }) + ReactiveAuthenticationManager denyAllAuthenticationManager() { + return (authentication) -> Mono.error(new UsernameNotFoundException(authentication.getName())); } - @ConditionalOnBean(SecurityWebFilterChain.class) - static final class ConditionalOnSecurityWebFilterChain { + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(WebFilterChainProxy.class) + @EnableWebFluxSecurity + static class EnableWebFluxSecurityConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 9583efcbc45..e8165ee1891 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -740,6 +740,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { .isEqualTo("aud"); } + @EnableWebFluxSecurity static class TestConfig { @Bean @@ -781,7 +782,6 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { } - @EnableWebFluxSecurity @Configuration(proxyBeanMethods = false) static class SecurityWebFilterChainConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java index dd5dd07ef32..006f7b228f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.EnableWebFluxSecurityConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -53,28 +52,36 @@ class ReactiveSecurityAutoConfigurationTests { } @Test - void backsOffWhenReactiveAuthenticationManagerNotPresent() { + void autoConfiguresDenyAllReactiveAuthenticationManagerWhenNoAlternativeIsAvailable() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ReactiveSecurityAutoConfiguration.class) - .doesNotHaveBean(EnableWebFluxSecurityConfiguration.class)); + .hasBean("denyAllAuthenticationManager")); } @Test void enablesWebFluxSecurityWhenUserDetailsServiceIsPresent() { - this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) - .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(WebFilterChainProxy.class); + assertThat(context).doesNotHaveBean("denyAllAuthenticationManager"); + }); } @Test void enablesWebFluxSecurityWhenReactiveAuthenticationManagerIsPresent() { this.contextRunner .withBean(ReactiveAuthenticationManager.class, () -> mock(ReactiveAuthenticationManager.class)) - .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + .run((context) -> { + assertThat(context).hasSingleBean(WebFilterChainProxy.class); + assertThat(context).doesNotHaveBean("denyAllAuthenticationManager"); + }); } @Test void enablesWebFluxSecurityWhenSecurityWebFilterChainIsPresent() { this.contextRunner.withBean(SecurityWebFilterChain.class, () -> mock(SecurityWebFilterChain.class)) - .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + .run((context) -> { + assertThat(context).hasSingleBean(WebFilterChainProxy.class); + assertThat(context).doesNotHaveBean("denyAllAuthenticationManager"); + }); } @Test