From 961da4e4280d6468a826e68e974a21b93db3b3ca Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 18 Jan 2024 14:14:39 +0000 Subject: [PATCH] Make user details only back off without custom username or password Closes gh-38864 --- ...veUserDetailsServiceAutoConfiguration.java | 37 ++++++++++++++++--- .../UserDetailsServiceAutoConfiguration.java | 36 ++++++++++++++++-- ...rDetailsServiceAutoConfigurationTests.java | 14 ++++++- ...rDetailsServiceAutoConfigurationTests.java | 21 ++++++++++- 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java index 17d6e1315f54..596f0d9c0b9d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -29,6 +29,7 @@ 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.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityProperties; @@ -58,13 +59,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" }) -@Conditional(ReactiveUserDetailsServiceAutoConfiguration.ReactiveUserDetailsServiceCondition.class) +@Conditional({ ReactiveUserDetailsServiceAutoConfiguration.RSocketEnabledOrReactiveWebApplication.class, + ReactiveUserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.class }) @EnableConfigurationProperties(SecurityProperties.class) public class ReactiveUserDetailsServiceAutoConfiguration { @@ -98,9 +98,9 @@ private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder return NOOP_PASSWORD_PREFIX + password; } - static class ReactiveUserDetailsServiceCondition extends AnyNestedCondition { + static class RSocketEnabledOrReactiveWebApplication extends AnyNestedCondition { - ReactiveUserDetailsServiceCondition() { + RSocketEnabledOrReactiveWebApplication() { super(ConfigurationPhase.REGISTER_BEAN); } @@ -116,4 +116,29 @@ static class ReactiveWebApplicationCondition { } + static final class MissingAlternativeOrUserPropertiesConfigured extends AnyNestedCondition { + + MissingAlternativeOrUserPropertiesConfigured() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnMissingClass({ + "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", + "org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" }) + static final class MissingAlternative { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "name") + static final class NameConfigured { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "password") + static final class PasswordConfigured { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java index 396b50856c34..9c4fef69ea0a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -25,12 +25,16 @@ import org.springframework.beans.factory.ObjectProvider; 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.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationProvider; @@ -53,9 +57,7 @@ */ @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" }) +@Conditional(MissingAlternativeOrUserPropertiesConfigured.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder") @@ -93,4 +95,30 @@ private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder return NOOP_PASSWORD_PREFIX + password; } + static final class MissingAlternativeOrUserPropertiesConfigured extends AnyNestedCondition { + + MissingAlternativeOrUserPropertiesConfigured() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnMissingClass({ + "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", + "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", + "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" }) + static final class MissingAlternative { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "name") + static final class NameConfigured { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "password") + static final class PasswordConfigured { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java index 96efa632a667..1b673b6dc962 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -121,6 +121,18 @@ void doesNotConfigureDefaultUserIfResourceServerIsPresent() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveUserDetailsService.class)); } + @Test + void configuresDefaultUserWhenResourceServerIsPresentAndUsernameIsConfigured() { + this.contextRunner.withPropertyValues("spring.security.user.name=carol") + .run((context) -> assertThat(context).hasSingleBean(ReactiveUserDetailsService.class)); + } + + @Test + void configuresDefaultUserWhenResourceServerIsPresentAndPasswordIsConfigured() { + this.contextRunner.withPropertyValues("spring.security.user.password=p4ssw0rd") + .run((context) -> assertThat(context).hasSingleBean(ReactiveUserDetailsService.class)); + } + @Test void userDetailsServiceWhenPasswordEncoderAbsentAndDefaultPassword() { this.contextRunner diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java index 660a2ea97247..9cffa54313ec 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -23,8 +23,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; @@ -176,6 +178,23 @@ void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresent() { .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); } + @Test + void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndUsernameConfigured() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) + .withPropertyValues("spring.security.user.name=alice") + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class))); + } + + @Test + void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndPasswordConfigured() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) + .withPropertyValues("spring.security.user.password=secret") + .run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class))); + } + private Function noOtherFormsOfAuthenticationOnTheClasspath() { return (contextRunner) -> contextRunner .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class,