From 57588c4e282318b4432e95fec1cd2e50e8ab44ee Mon Sep 17 00:00:00 2001 From: Siva Sai Udayagiri Date: Mon, 6 Oct 2025 16:44:36 -0400 Subject: [PATCH 1/4] Honor @Primary for UserDetailsService and UserDetailsPasswordService Signed-off-by: Siva Sai Udayagiri --- ...alizeUserDetailsBeanManagerConfigurer.java | 80 ++++++--- ...UserDetailsBeanManagerConfigurerTests.java | 153 ++++++++++++++++++ 2 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index 6f3d7141447..0509fefaddf 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -21,6 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -33,9 +34,12 @@ import org.springframework.security.crypto.password.PasswordEncoder; /** - * Lazily initializes the global authentication with a {@link UserDetailsService} if it is - * not yet configured and there is only a single Bean of that type. Optionally, if a - * {@link PasswordEncoder} is defined will wire this up too. + * Lazily initializes the global authentication with a {@link UserDetailsService}. If + * multiple beans of that type exist, the container's autowire rules are used to select a + * single candidate (e.g. {@code @Primary}). If no single candidate can be resolved, the + * configurer logs a warning and does not auto-wire. Optionally wires a + * {@link PasswordEncoder}, {@link UserDetailsPasswordService}, and + * {@link CompromisedPasswordChecker} when available. * * @author Rob Winch * @author Ngoc Nhan @@ -48,9 +52,6 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon private final ApplicationContext context; - /** - * @param context - */ InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) { this.context = context; } @@ -68,6 +69,7 @@ class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigu public void configure(AuthenticationManagerBuilder auth) { String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context .getBeanNamesForType(UserDetailsService.class); + if (auth.isConfigured()) { if (beanNames.length > 0) { this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. " @@ -83,18 +85,27 @@ public void configure(AuthenticationManagerBuilder auth) { if (beanNames.length == 0) { return; } - else if (beanNames.length > 1) { - this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " - + "Global Authentication Manager will not use a UserDetailsService for username/password login. " - + "Consider publishing a single UserDetailsService bean.", beanNames.length, - Arrays.toString(beanNames))); + + // Try to resolve a single candidate using the container's rules (@Primary, + // etc.) + UserDetailsService userDetailsService = getAutowireCandidateOrNull(UserDetailsService.class); + + // If ambiguous or otherwise not resolvable, keep the warn-and-skip behavior + if (userDetailsService == null) { + if (beanNames.length > 1) { + this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " + + "Global Authentication Manager will not use a UserDetailsService for username/password login. " + + "Consider publishing a single (or primary) UserDetailsService bean.", beanNames.length, + Arrays.toString(beanNames))); + } return; } - UserDetailsService userDetailsService = InitializeUserDetailsBeanManagerConfigurer.this.context - .getBean(beanNames[0], UserDetailsService.class); - PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); - UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class); - CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class); + + PasswordEncoder passwordEncoder = getBeanIfUnique(PasswordEncoder.class); + // Also resolve UDPS via container so @Primary is honored + UserDetailsPasswordService passwordManager = getAutowireCandidateOrNull(UserDetailsPasswordService.class); + CompromisedPasswordChecker passwordChecker = getBeanIfUnique(CompromisedPasswordChecker.class); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); if (passwordEncoder != null) { provider.setPasswordEncoder(passwordEncoder); @@ -107,18 +118,47 @@ else if (beanNames.length > 1) { } provider.afterPropertiesSet(); auth.authenticationProvider(provider); + + String selectedName = resolveBeanName(beanNames, userDetailsService); this.logger.info(LogMessage.format( - "Global AuthenticationManager configured with UserDetailsService bean with name %s", beanNames[0])); + "Global AuthenticationManager configured with UserDetailsService bean with name %s", selectedName)); } /** - * @return a bean of the requested class if there's just a single registered - * component, null otherwise. + * Resolve a single autowire candidate for the given type (honors + * {@code @Primary}). Returns {@code null} if ambiguous or not present. */ - private T getBeanOrNull(Class type) { + private T getAutowireCandidateOrNull(Class type) { + try { + return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfAvailable(); + } + catch (BeansException ex) { + return null; + } + } + + /** + * Return a bean of the requested class if there's exactly one registered + * component; {@code null} otherwise. + */ + private T getBeanIfUnique(Class type) { return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique(); } + private String resolveBeanName(String[] candidates, Object instance) { + for (String name : candidates) { + try { + Object bean = InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(name); + if (bean == instance) { + return name; + } + } + catch (BeansException ignored) { + } + } + return instance.getClass().getName(); + } + } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java new file mode 100644 index 00000000000..7f1d3cd377a --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2004-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.authentication.configuration; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.password.CompromisedPasswordChecker; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +class InitializeUserDetailsBeanManagerConfigurerTests { + + private static ObjectPostProcessor opp() { + return new ObjectPostProcessor<>() { + @Override + public O postProcess(O object) { + return object; + } + }; + } + + @SuppressWarnings("unchecked") + @Test + void whenMultipleUdsAndOneResolvableCandidate_thenPrimaryIsAutoWired() throws Exception { + ApplicationContext ctx = mock(ApplicationContext.class); + given(ctx.getBeanNamesForType(UserDetailsService.class)).willReturn(new String[] { "udsA", "udsB" }); + + PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + + InMemoryUserDetailsManager primary = new InMemoryUserDetailsManager( + User.withUsername("alice").passwordEncoder(encoder::encode).password("pw").roles("USER").build()); + InMemoryUserDetailsManager secondary = new InMemoryUserDetailsManager(); + + ObjectProvider udsProvider = (ObjectProvider) mock( + ObjectProvider.class); + given(ctx.getBeanProvider(UserDetailsService.class)).willReturn(udsProvider); + given(udsProvider.getIfAvailable()).willReturn(primary); // container picks single + // candidate + + // resolveBeanName(..) path + given(ctx.getBean("udsA")).willReturn(secondary); + given(ctx.getBean("udsB")).willReturn(primary); + + ObjectProvider peProvider = (ObjectProvider) mock(ObjectProvider.class); + given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider); + given(peProvider.getIfUnique()).willReturn(encoder); + + // Stub optional providers to avoid NPEs + ObjectProvider udpsProvider = (ObjectProvider) mock( + ObjectProvider.class); + given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider); + given(udpsProvider.getIfAvailable()).willReturn(null); + + ObjectProvider cpcProvider = (ObjectProvider) mock( + ObjectProvider.class); + given(ctx.getBeanProvider(CompromisedPasswordChecker.class)).willReturn(cpcProvider); + given(cpcProvider.getIfUnique()).willReturn(null); + + AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder(opp()); + new InitializeUserDetailsBeanManagerConfigurer(ctx).new InitializeUserDetailsManagerConfigurer() + .configure(builder); + + AuthenticationManager manager = builder.build(); + + // DaoAuthenticationProvider registered + assertThat(manager).isInstanceOf(ProviderManager.class); + List providers = ((ProviderManager) manager).getProviders(); + assertThat(providers) + .anySatisfy((p) -> assertThat(p.getClass().getSimpleName()).isEqualTo("DaoAuthenticationProvider")); + + // Auth works with the primary UDS + encoder + var auth = manager.authenticate(new UsernamePasswordAuthenticationToken("alice", "pw")); + assertThat(auth.isAuthenticated()).isTrue(); + } + + @SuppressWarnings("unchecked") + @Test + void whenMultipleUdsAndNoSingleCandidate_thenSkipAutoWiring() throws Exception { + ApplicationContext ctx = mock(ApplicationContext.class); + given(ctx.getBeanNamesForType(UserDetailsService.class)).willReturn(new String[] { "udsA", "udsB" }); + + ObjectProvider udsProvider = (ObjectProvider) mock( + ObjectProvider.class); + given(ctx.getBeanProvider(UserDetailsService.class)).willReturn(udsProvider); + given(udsProvider.getIfAvailable()).willReturn(null); // ambiguous → no single + // candidate + + // Also stub other providers to null + ObjectProvider peProvider = (ObjectProvider) mock(ObjectProvider.class); + given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider); + given(peProvider.getIfUnique()).willReturn(null); + + ObjectProvider udpsProvider = (ObjectProvider) mock( + ObjectProvider.class); + given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider); + given(udpsProvider.getIfAvailable()).willReturn(null); + + ObjectProvider cpcProvider = (ObjectProvider) mock( + ObjectProvider.class); + given(ctx.getBeanProvider(CompromisedPasswordChecker.class)).willReturn(cpcProvider); + given(cpcProvider.getIfUnique()).willReturn(null); + + AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder(opp()); + new InitializeUserDetailsBeanManagerConfigurer(ctx).new InitializeUserDetailsManagerConfigurer() + .configure(builder); + + AuthenticationManager manager = builder.build(); + + // Success condition: nothing auto-registered. + if (manager == null) { + assertThat(manager).isNull(); + } + else if (manager instanceof ProviderManager pm) { + assertThat(pm.getProviders()) + .noneMatch((p) -> p.getClass().getSimpleName().equals("DaoAuthenticationProvider")); + } + else { + assertThat(manager.getClass().getSimpleName()).isNotEqualTo("ProviderManager"); + } + } + +} From 27574ee617a04b4a6769f6c1a410e99527bf74e7 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:09:41 -0600 Subject: [PATCH 2/4] Polish UserDetailsService Selection Logic This commit rearranges the branches to reduce nesting Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- ...alizeUserDetailsBeanManagerConfigurer.java | 30 ++++++++----------- ...UserDetailsBeanManagerConfigurerTests.java | 4 +-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index 0509fefaddf..d5b63184890 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -70,34 +70,30 @@ public void configure(AuthenticationManagerBuilder auth) { String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context .getBeanNamesForType(UserDetailsService.class); - if (auth.isConfigured()) { - if (beanNames.length > 0) { - this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. " - + "UserDetailsService beans will not be used by Spring Security for automatically configuring username/password login. " - + "Consider removing the AuthenticationProvider bean. " - + "Alternatively, consider using the UserDetailsService in a manually instantiated DaoAuthenticationProvider. " - + "If the current configuration is intentional, to turn off this warning, " - + "increase the logging level of 'org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer' to ERROR"); - } + if (beanNames.length == 0) { return; } - if (beanNames.length == 0) { + if (auth.isConfigured()) { + this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. " + + "UserDetailsService beans will not be used by Spring Security for automatically configuring username/password login. " + + "Consider removing the AuthenticationProvider bean. " + + "Alternatively, consider using the UserDetailsService in a manually instantiated DaoAuthenticationProvider. " + + "If the current configuration is intentional, to turn off this warning, " + + "increase the logging level of 'org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer' to ERROR"); return; } // Try to resolve a single candidate using the container's rules (@Primary, // etc.) - UserDetailsService userDetailsService = getAutowireCandidateOrNull(UserDetailsService.class); + UserDetailsService userDetailsService = getBeanIfUnique(UserDetailsService.class); // If ambiguous or otherwise not resolvable, keep the warn-and-skip behavior if (userDetailsService == null) { - if (beanNames.length > 1) { - this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " - + "Global Authentication Manager will not use a UserDetailsService for username/password login. " - + "Consider publishing a single (or primary) UserDetailsService bean.", beanNames.length, - Arrays.toString(beanNames))); - } + this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " + + "Global Authentication Manager will not use a UserDetailsService for username/password login. " + + "Consider publishing a single (or primary) UserDetailsService bean.", beanNames.length, + Arrays.toString(beanNames))); return; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java index 7f1d3cd377a..717858e896a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java @@ -65,8 +65,8 @@ void whenMultipleUdsAndOneResolvableCandidate_thenPrimaryIsAutoWired() throws Ex ObjectProvider udsProvider = (ObjectProvider) mock( ObjectProvider.class); given(ctx.getBeanProvider(UserDetailsService.class)).willReturn(udsProvider); - given(udsProvider.getIfAvailable()).willReturn(primary); // container picks single - // candidate + given(udsProvider.getIfUnique()).willReturn(primary); // container picks single + // candidate // resolveBeanName(..) path given(ctx.getBean("udsA")).willReturn(secondary); From 581d666ded239b2979dedc87d76156637ed8fa42 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:14:36 -0600 Subject: [PATCH 3/4] Reduce Comments Given that the codebase uses getBeanIfUnique logic in many places, it is a little noisy to clarify how it works in just this location. Instead, the previous commit simplifies the branching logic, reducing the need for clarification of what could otherwise be surprising. Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- .../InitializeUserDetailsBeanManagerConfigurer.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index d5b63184890..300914e841d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -84,11 +84,7 @@ public void configure(AuthenticationManagerBuilder auth) { return; } - // Try to resolve a single candidate using the container's rules (@Primary, - // etc.) UserDetailsService userDetailsService = getBeanIfUnique(UserDetailsService.class); - - // If ambiguous or otherwise not resolvable, keep the warn-and-skip behavior if (userDetailsService == null) { this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " + "Global Authentication Manager will not use a UserDetailsService for username/password login. " @@ -98,7 +94,6 @@ public void configure(AuthenticationManagerBuilder auth) { } PasswordEncoder passwordEncoder = getBeanIfUnique(PasswordEncoder.class); - // Also resolve UDPS via container so @Primary is honored UserDetailsPasswordService passwordManager = getAutowireCandidateOrNull(UserDetailsPasswordService.class); CompromisedPasswordChecker passwordChecker = getBeanIfUnique(CompromisedPasswordChecker.class); From 829cac4986112718dee3821fd8bb6e2be5265416 Mon Sep 17 00:00:00 2001 From: Siva Sai Udayagiri Date: Wed, 22 Oct 2025 15:34:45 -0400 Subject: [PATCH 4/4] Honor @Primary for UserDetailsService and UserDetailsPasswordService in InitializeUserDetailsBeanManagerConfigurer (#17902) Signed-off-by: Siva Sai Udayagiri --- ...alizeUserDetailsBeanManagerConfigurer.java | 102 ++++++++---------- ...UserDetailsBeanManagerConfigurerTests.java | 61 +++++------ 2 files changed, 71 insertions(+), 92 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index 300914e841d..a749f937150 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -6,12 +6,6 @@ * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.security.config.annotation.authentication.configuration; @@ -21,7 +15,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -34,15 +27,15 @@ import org.springframework.security.crypto.password.PasswordEncoder; /** - * Lazily initializes the global authentication with a {@link UserDetailsService}. If - * multiple beans of that type exist, the container's autowire rules are used to select a - * single candidate (e.g. {@code @Primary}). If no single candidate can be resolved, the - * configurer logs a warning and does not auto-wire. Optionally wires a - * {@link PasswordEncoder}, {@link UserDetailsPasswordService}, and - * {@link CompromisedPasswordChecker} when available. + * Lazily initializes the global authentication with a {@link UserDetailsService} + * if it is not yet configured. Honors {@code @Primary} when multiple + * {@link UserDetailsService} (or {@link UserDetailsPasswordService}) beans are present. + * If a single {@link PasswordEncoder} or {@link CompromisedPasswordChecker} bean is + * available, those are wired as well. * * @author Rob Winch * @author Ngoc Nhan + * @author You * @since 4.1 */ @Order(InitializeUserDetailsBeanManagerConfigurer.DEFAULT_ORDER) @@ -68,33 +61,54 @@ class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigu @Override public void configure(AuthenticationManagerBuilder auth) { String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context - .getBeanNamesForType(UserDetailsService.class); + .getBeanNamesForType(UserDetailsService.class); + // If user configured an AuthenticationProvider already, warn and bail + if (auth.isConfigured()) { + if (beanNames.length > 0) { + this.logger.warn( + "Global AuthenticationManager configured with an AuthenticationProvider bean. " + + "UserDetailsService beans will not be used by Spring Security for automatically configuring username/password login. " + + "Consider removing the AuthenticationProvider bean or configure DaoAuthenticationProvider manually."); + } + return; + } + + // No UDS beans — nothing to do if (beanNames.length == 0) { return; } - if (auth.isConfigured()) { - this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. " - + "UserDetailsService beans will not be used by Spring Security for automatically configuring username/password login. " - + "Consider removing the AuthenticationProvider bean. " - + "Alternatively, consider using the UserDetailsService in a manually instantiated DaoAuthenticationProvider. " - + "If the current configuration is intentional, to turn off this warning, " - + "increase the logging level of 'org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer' to ERROR"); + /* + * Try to resolve a single autowire-candidate UDS from the container. + * getIfAvailable() returns: + * - the bean if there is exactly one, or + * - the @Primary bean if there are multiple and one is marked primary, + * - otherwise null. + */ + UserDetailsService userDetailsService = getAutowireCandidateOrNull(UserDetailsService.class); + + // If still ambiguous and we have multiple beans, keep current (warn + skip) + if (userDetailsService == null && beanNames.length > 1) { + this.logger.warn(LogMessage.format( + "Found %s UserDetailsService beans, with names %s. " + + "Global Authentication Manager will not use a UserDetailsService for username/password login. " + + "Consider publishing a single (or @Primary) UserDetailsService bean.", + beanNames.length, Arrays.toString(beanNames))); return; } - UserDetailsService userDetailsService = getBeanIfUnique(UserDetailsService.class); + // If there is exactly one bean and getIfAvailable returned null (shouldn't happen), + // fall back to retrieving that single bean by name. if (userDetailsService == null) { - this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. " - + "Global Authentication Manager will not use a UserDetailsService for username/password login. " - + "Consider publishing a single (or primary) UserDetailsService bean.", beanNames.length, - Arrays.toString(beanNames))); - return; + userDetailsService = InitializeUserDetailsBeanManagerConfigurer.this.context + .getBean(beanNames[0], UserDetailsService.class); } PasswordEncoder passwordEncoder = getBeanIfUnique(PasswordEncoder.class); + // Honor @Primary for UDPS as well UserDetailsPasswordService passwordManager = getAutowireCandidateOrNull(UserDetailsPasswordService.class); + // Keep "unique only" semantics for optional checker CompromisedPasswordChecker passwordChecker = getBeanIfUnique(CompromisedPasswordChecker.class); DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); @@ -108,48 +122,20 @@ public void configure(AuthenticationManagerBuilder auth) { provider.setCompromisedPasswordChecker(passwordChecker); } provider.afterPropertiesSet(); + auth.authenticationProvider(provider); - String selectedName = resolveBeanName(beanNames, userDetailsService); this.logger.info(LogMessage.format( - "Global AuthenticationManager configured with UserDetailsService bean with name %s", selectedName)); + "Global AuthenticationManager configured with UserDetailsService bean (auto-selected).")); } - /** - * Resolve a single autowire candidate for the given type (honors - * {@code @Primary}). Returns {@code null} if ambiguous or not present. - */ private T getAutowireCandidateOrNull(Class type) { - try { - return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfAvailable(); - } - catch (BeansException ex) { - return null; - } + return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfAvailable(); } - /** - * Return a bean of the requested class if there's exactly one registered - * component; {@code null} otherwise. - */ private T getBeanIfUnique(Class type) { return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique(); } - - private String resolveBeanName(String[] candidates, Object instance) { - for (String name : candidates) { - try { - Object bean = InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(name); - if (bean == instance) { - return name; - } - } - catch (BeansException ignored) { - } - } - return instance.getClass().getName(); - } - } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java index 717858e896a..1b51a89cccf 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java @@ -6,12 +6,6 @@ * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.springframework.security.config.annotation.authentication.configuration; @@ -44,9 +38,7 @@ class InitializeUserDetailsBeanManagerConfigurerTests { private static ObjectPostProcessor opp() { return new ObjectPostProcessor<>() { @Override - public O postProcess(O object) { - return object; - } + public O postProcess(O object) { return object; } }; } @@ -62,34 +54,35 @@ void whenMultipleUdsAndOneResolvableCandidate_thenPrimaryIsAutoWired() throws Ex User.withUsername("alice").passwordEncoder(encoder::encode).password("pw").roles("USER").build()); InMemoryUserDetailsManager secondary = new InMemoryUserDetailsManager(); - ObjectProvider udsProvider = (ObjectProvider) mock( - ObjectProvider.class); + ObjectProvider udsProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(UserDetailsService.class)).willReturn(udsProvider); - given(udsProvider.getIfUnique()).willReturn(primary); // container picks single - // candidate + given(udsProvider.getIfAvailable()).willReturn(primary); // container picks single candidate // resolveBeanName(..) path given(ctx.getBean("udsA")).willReturn(secondary); given(ctx.getBean("udsB")).willReturn(primary); - ObjectProvider peProvider = (ObjectProvider) mock(ObjectProvider.class); + ObjectProvider peProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider); given(peProvider.getIfUnique()).willReturn(encoder); // Stub optional providers to avoid NPEs - ObjectProvider udpsProvider = (ObjectProvider) mock( - ObjectProvider.class); + ObjectProvider udpsProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider); given(udpsProvider.getIfAvailable()).willReturn(null); - ObjectProvider cpcProvider = (ObjectProvider) mock( - ObjectProvider.class); + ObjectProvider cpcProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(CompromisedPasswordChecker.class)).willReturn(cpcProvider); given(cpcProvider.getIfUnique()).willReturn(null); AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder(opp()); - new InitializeUserDetailsBeanManagerConfigurer(ctx).new InitializeUserDetailsManagerConfigurer() - .configure(builder); + new InitializeUserDetailsBeanManagerConfigurer(ctx) + .new InitializeUserDetailsManagerConfigurer() + .configure(builder); AuthenticationManager manager = builder.build(); @@ -97,7 +90,7 @@ void whenMultipleUdsAndOneResolvableCandidate_thenPrimaryIsAutoWired() throws Ex assertThat(manager).isInstanceOf(ProviderManager.class); List providers = ((ProviderManager) manager).getProviders(); assertThat(providers) - .anySatisfy((p) -> assertThat(p.getClass().getSimpleName()).isEqualTo("DaoAuthenticationProvider")); + .anySatisfy((p) -> assertThat(p.getClass().getSimpleName()).isEqualTo("DaoAuthenticationProvider")); // Auth works with the primary UDS + encoder var auth = manager.authenticate(new UsernamePasswordAuthenticationToken("alice", "pw")); @@ -110,30 +103,31 @@ void whenMultipleUdsAndNoSingleCandidate_thenSkipAutoWiring() throws Exception { ApplicationContext ctx = mock(ApplicationContext.class); given(ctx.getBeanNamesForType(UserDetailsService.class)).willReturn(new String[] { "udsA", "udsB" }); - ObjectProvider udsProvider = (ObjectProvider) mock( - ObjectProvider.class); + ObjectProvider udsProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(UserDetailsService.class)).willReturn(udsProvider); - given(udsProvider.getIfAvailable()).willReturn(null); // ambiguous → no single - // candidate + given(udsProvider.getIfAvailable()).willReturn(null); // ambiguous → no single candidate // Also stub other providers to null - ObjectProvider peProvider = (ObjectProvider) mock(ObjectProvider.class); + ObjectProvider peProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider); given(peProvider.getIfUnique()).willReturn(null); - ObjectProvider udpsProvider = (ObjectProvider) mock( - ObjectProvider.class); + ObjectProvider udpsProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider); given(udpsProvider.getIfAvailable()).willReturn(null); - ObjectProvider cpcProvider = (ObjectProvider) mock( - ObjectProvider.class); + ObjectProvider cpcProvider = + (ObjectProvider) mock(ObjectProvider.class); given(ctx.getBeanProvider(CompromisedPasswordChecker.class)).willReturn(cpcProvider); given(cpcProvider.getIfUnique()).willReturn(null); AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder(opp()); - new InitializeUserDetailsBeanManagerConfigurer(ctx).new InitializeUserDetailsManagerConfigurer() - .configure(builder); + new InitializeUserDetailsBeanManagerConfigurer(ctx) + .new InitializeUserDetailsManagerConfigurer() + .configure(builder); AuthenticationManager manager = builder.build(); @@ -143,11 +137,10 @@ void whenMultipleUdsAndNoSingleCandidate_thenSkipAutoWiring() throws Exception { } else if (manager instanceof ProviderManager pm) { assertThat(pm.getProviders()) - .noneMatch((p) -> p.getClass().getSimpleName().equals("DaoAuthenticationProvider")); + .noneMatch((p) -> p.getClass().getSimpleName().equals("DaoAuthenticationProvider")); } else { assertThat(manager.getClass().getSimpleName()).isNotEqualTo("ProviderManager"); } } - }