Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,12 +27,15 @@
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 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)
Expand All @@ -48,9 +45,6 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon

private final ApplicationContext context;

/**
* @param context
*/
InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
this.context = context;
}
Expand All @@ -67,34 +61,56 @@ 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. "
+ "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");
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;
}
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 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 = InitializeUserDetailsBeanManagerConfigurer.this.context
.getBean(beanNames[0], UserDetailsService.class);
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.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) {
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this changes the existing semantics by calling getIfAvailable instead of getIfUnique. The ObjectProvider JavaDoc implies that it accounts for @Primary:

	/**
	 * Return an instance (possibly shared or independent) of the object
	 * managed by this factory.
	 * @return an instance of the bean, or {@code null} if not available or
	 * not unique (i.e. multiple candidates found **with none marked as primary**)
	 * @throws BeansException in case of creation errors
	 * @see #getObject()
	 */

Can you confirm with unit tests that changing to getIfAvailable is necessary to support @Primary?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @jzheaux! I added unit tests to show why we need to rely on the container’s autowire-candidate resolution to honor @primary.

• With getIfUnique(...) when there are two UserDetailsService beans and one is @primary, it still returns null (multiple names), so the global AuthenticationManager isn’t configured.
• With getIfAvailable(...) it delegates to autowire-candidate selection (same as normal injection), so the @primary bean is chosen, and the AuthenticationManager is configured.

New tests:

  • whenMultipleUdsAndOneResolvableCandidate_thenPrimaryIsAutoWired
  • whenMultipleUdsAndNoSingleCandidate_thenSkipAutoWiring

Location: spring-security-config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurerTests.java

This keeps behavior the same for the single-bean case, and for multiple beans without a single resolvable candidate we still skip auto-wiring (and log). Please take another look and let me know if you’d prefer a different approach.

// Keep "unique only" semantics for optional checker
CompromisedPasswordChecker passwordChecker = getBeanIfUnique(CompromisedPasswordChecker.class);

DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
if (passwordEncoder != null) {
provider.setPasswordEncoder(passwordEncoder);
Expand All @@ -106,19 +122,20 @@ else if (beanNames.length > 1) {
provider.setCompromisedPasswordChecker(passwordChecker);
}
provider.afterPropertiesSet();

auth.authenticationProvider(provider);

this.logger.info(LogMessage.format(
"Global AuthenticationManager configured with UserDetailsService bean with name %s", beanNames[0]));
"Global AuthenticationManager configured with UserDetailsService bean (auto-selected)."));
}

/**
* @return a bean of the requested class if there's just a single registered
* component, null otherwise.
*/
private <T> T getBeanOrNull(Class<T> type) {
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique();
private <T> T getAutowireCandidateOrNull(Class<T> type) {
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfAvailable();
}

private <T> T getBeanIfUnique(Class<T> type) {
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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
*/

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<Object> opp() {
return new ObjectPostProcessor<>() {
@Override
public <O> 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<UserDetailsService> udsProvider =
(ObjectProvider<UserDetailsService>) 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<PasswordEncoder> peProvider =
(ObjectProvider<PasswordEncoder>) mock(ObjectProvider.class);
given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider);
given(peProvider.getIfUnique()).willReturn(encoder);

// Stub optional providers to avoid NPEs
ObjectProvider<UserDetailsPasswordService> udpsProvider =
(ObjectProvider<UserDetailsPasswordService>) mock(ObjectProvider.class);
given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider);
given(udpsProvider.getIfAvailable()).willReturn(null);

ObjectProvider<CompromisedPasswordChecker> cpcProvider =
(ObjectProvider<CompromisedPasswordChecker>) 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<UserDetailsService> udsProvider =
(ObjectProvider<UserDetailsService>) 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<PasswordEncoder> peProvider =
(ObjectProvider<PasswordEncoder>) mock(ObjectProvider.class);
given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider);
given(peProvider.getIfUnique()).willReturn(null);

ObjectProvider<UserDetailsPasswordService> udpsProvider =
(ObjectProvider<UserDetailsPasswordService>) mock(ObjectProvider.class);
given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider);
given(udpsProvider.getIfAvailable()).willReturn(null);

ObjectProvider<CompromisedPasswordChecker> cpcProvider =
(ObjectProvider<CompromisedPasswordChecker>) 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");
}
}
}
Loading