Skip to content

Commit

Permalink
Make user details service auto-configs back off more readily
Browse files Browse the repository at this point in the history
Previously auto-configuration of a user details service (imperative
or reactive) would only back off on the presence of certain beans.
This led to situations where the im-memory service was
auto-configured and the default password was logged even though
another authentication mechanism was in use.

This commit updates the auto-configuration so that it backs off
when depending on Spring Security's OAuth2 Client and OAuth2
Resource Server modules. In the imperative case it will also back
off when depending on the SAML 2 provider.

Closes gh-35338
  • Loading branch information
wilkinsona committed Sep 26, 2023
1 parent ab3c579 commit ecc6707
Show file tree
Hide file tree
Showing 15 changed files with 215 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -52,15 +55,14 @@ class CloudFoundryReactiveHealthEndpointWebExtensionTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withPropertyValues("VCAP_APPLICATION={}")
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class,
ReactiveUserDetailsServiceAutoConfiguration.class, WebFluxAutoConfiguration.class,
JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class,
WebFluxAutoConfiguration.class, JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
ReactiveCloudFoundryActuatorAutoConfigurationTests.WebClientCustomizerConfig.class,
WebClientAutoConfiguration.class, ManagementContextAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
ReactiveCloudFoundryActuatorAutoConfiguration.class))
.withUserConfiguration(TestHealthIndicator.class);
.withUserConfiguration(TestHealthIndicator.class, UserDetailsServiceConfiguration.class);

@Test
void healthComponentsAlwaysPresent() {
Expand All @@ -82,4 +84,15 @@ public Health health() {

}

@Configuration(proxyBeanMethods = false)
static class UserDetailsServiceConfiguration {

@Bean
MapReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(
User.withUsername("alice").password("secret").roles("admin").build());
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
Expand All @@ -61,6 +60,8 @@
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.test.util.ReflectionTestUtils;
Expand All @@ -84,15 +85,16 @@ class ReactiveCloudFoundryActuatorAutoConfigurationTests {
private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString();

private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class,
ReactiveUserDetailsServiceAutoConfiguration.class, WebFluxAutoConfiguration.class,
JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, WebClientCustomizerConfig.class,
WebClientAutoConfiguration.class, ManagementContextAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
InfoContributorAutoConfiguration.class, InfoEndpointAutoConfiguration.class,
ProjectInfoAutoConfiguration.class, ReactiveCloudFoundryActuatorAutoConfiguration.class));
.withConfiguration(
AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, WebFluxAutoConfiguration.class,
JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, WebClientCustomizerConfig.class,
WebClientAutoConfiguration.class, ManagementContextAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
InfoContributorAutoConfiguration.class, InfoEndpointAutoConfiguration.class,
ProjectInfoAutoConfiguration.class, ReactiveCloudFoundryActuatorAutoConfiguration.class))
.withUserConfiguration(UserDetailsServiceConfiguration.class);

private static final String BASE_PATH = "/cloudfoundryapplication";

Expand Down Expand Up @@ -358,4 +360,15 @@ WebClientCustomizer webClientCustomizer() {

}

@Configuration(proxyBeanMethods = false)
static class UserDetailsServiceConfiguration {

@Bean
MapReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(
User.withUsername("alice").password("secret").roles("admin").build());
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.springframework.boot.context.annotation.UserConfigurations;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -59,6 +60,7 @@ void healthEndpointWebExtensionIsAutoConfigured() {
}

@Test
@ClassPathExclusions({ "spring-security-oauth2-client-*.jar", "spring-security-oauth2-resource-server-*.jar" })
void healthEndpointReactiveWebExtensionIsAutoConfigured() {
reactiveWebRunner()
.run((context) -> assertThat(context).hasSingleBean(ReactiveHealthEndpointWebExtension.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
Expand All @@ -48,6 +47,8 @@
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterChainProxy;
import org.springframework.web.server.ServerWebExchange;
Expand All @@ -70,8 +71,8 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class,
WebFluxAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class,
ReactiveManagementWebSecurityAutoConfiguration.class));
ReactiveSecurityAutoConfiguration.class, ReactiveManagementWebSecurityAutoConfiguration.class))
.withUserConfiguration(UserDetailsServiceConfiguration.class);

@Test
void permitAllForHealth() {
Expand Down Expand Up @@ -155,6 +156,17 @@ protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttp

}

@Configuration(proxyBeanMethods = false)
static class UserDetailsServiceConfiguration {

@Bean
MapReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(
User.withUsername("alice").password("secret").roles("admin").build());
}

}

@Configuration(proxyBeanMethods = false)
static class CustomSecurityConfiguration {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.web.reactive.server.WebTestClient;

Expand Down Expand Up @@ -100,8 +101,8 @@ protected final WebApplicationContextRunner getContextRunner() {
return createContextRunner().withPropertyValues("management.endpoints.web.exposure.include=*")
.withUserConfiguration(BaseConfiguration.class, SecurityConfiguration.class)
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, SecurityAutoConfiguration.class,
UserDetailsServiceAutoConfiguration.class, EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class));
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ManagementContextAutoConfiguration.class));

}

Expand Down Expand Up @@ -189,6 +190,12 @@ public EndpointServlet get() {
@Configuration(proxyBeanMethods = false)
static class SecurityConfiguration {

@Bean
InMemoryUserDetailsManager userDetailsManager() {
return new InMemoryUserDetailsManager(
User.withUsername("user").password("{noop}password").roles("admin").build());
}

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand All @@ -37,6 +38,7 @@
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;

Expand Down Expand Up @@ -92,6 +94,13 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http.build();
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebFilterChainProxy.class)
@EnableWebFluxSecurity
static class EnableWebFluxSecurityConfiguration {

}

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
Expand Down Expand Up @@ -57,11 +58,12 @@
*/
@AutoConfiguration(before = ReactiveSecurityAutoConfiguration.class, after = RSocketMessagingAutoConfiguration.class)
@ConditionalOnClass({ ReactiveAuthenticationManager.class })
@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
"org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" })
@ConditionalOnMissingBean(
value = { ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class,
ReactiveAuthenticationManagerResolver.class },
type = { "org.springframework.security.oauth2.jwt.ReactiveJwtDecoder",
"org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" })
type = { "org.springframework.security.oauth2.jwt.ReactiveJwtDecoder" })
@Conditional(ReactiveUserDetailsServiceAutoConfiguration.ReactiveUserDetailsServiceCondition.class)
@EnableConfigurationProperties(SecurityProperties.class)
public class ReactiveUserDetailsServiceAutoConfiguration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
Expand All @@ -43,9 +44,7 @@
/**
* {@link EnableAutoConfiguration Auto-configuration} for a Spring Security in-memory
* {@link AuthenticationManager}. Adds an {@link InMemoryUserDetailsManager} with a
* default user and generated password. This can be disabled by providing a bean of type
* {@link AuthenticationManager}, {@link AuthenticationProvider} or
* {@link UserDetailsService}.
* default user and generated password.
*
* @author Dave Syer
* @author Rob Winch
Expand All @@ -54,14 +53,12 @@
*/
@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class },
type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder")
public class UserDetailsServiceAutoConfiguration {

private static final String NOOP_PASSWORD_PREFIX = "{noop}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
Expand Down Expand Up @@ -59,8 +60,11 @@ void autoConfigurationConditionalOnClassOauth2Authorization() {
}

@Test
@ClassPathExclusions({ "spring-security-oauth2-client-*.jar", "spring-security-oauth2-resource-server-*.jar",
"spring-security-saml2-service-provider-*.jar" })
void autoConfigurationDoesNotCauseUserDetailsServiceToBackOff() {
this.contextRunner.run((context) -> assertThat(context).hasBean("inMemoryUserDetailsManager"));
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(UserDetailsServiceAutoConfiguration.class)
.hasBean("inMemoryUserDetailsManager"));
}

@Test
Expand Down
Loading

0 comments on commit ecc6707

Please sign in to comment.