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 @@ -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;
Expand All @@ -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
Expand All @@ -48,9 +52,6 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon

private final ApplicationContext context;

/**
* @param context
*/
InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
this.context = context;
}
Expand All @@ -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. "
Expand All @@ -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);
Expand All @@ -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> T getBeanOrNull(Class<T> type) {
private <T> T getAutowireCandidateOrNull(Class<T> 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> T getBeanIfUnique(Class<T> 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();
}

}

}
Original file line number Diff line number Diff line change
@@ -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<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");
}
}

}