Skip to content

Commit 2382273

Browse files
Honor @primary for UserDetailsService and UserDetailsPasswordService in InitializeUserDetailsBeanManagerConfigurer
Signed-off-by: Siva Sai Udayagiri <[email protected]>
1 parent d5c5bb2 commit 2382273

File tree

2 files changed

+213
-20
lines changed

2 files changed

+213
-20
lines changed

config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.apache.commons.logging.Log;
2222
import org.apache.commons.logging.LogFactory;
2323

24+
import org.springframework.beans.BeansException;
2425
import org.springframework.context.ApplicationContext;
2526
import org.springframework.core.Ordered;
2627
import org.springframework.core.annotation.Order;
@@ -33,9 +34,12 @@
3334
import org.springframework.security.crypto.password.PasswordEncoder;
3435

3536
/**
36-
* Lazily initializes the global authentication with a {@link UserDetailsService} if it is
37-
* not yet configured and there is only a single Bean of that type. Optionally, if a
38-
* {@link PasswordEncoder} is defined will wire this up too.
37+
* Lazily initializes the global authentication with a {@link UserDetailsService}. If
38+
* multiple beans of that type exist, the container's autowire rules are used to select a
39+
* single candidate (e.g. {@code @Primary}). If no single candidate can be resolved, the
40+
* configurer logs a warning and does not auto-wire. Optionally wires a
41+
* {@link PasswordEncoder}, {@link UserDetailsPasswordService}, and
42+
* {@link CompromisedPasswordChecker} when available.
3943
*
4044
* @author Rob Winch
4145
* @author Ngoc Nhan
@@ -48,9 +52,6 @@ class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationCon
4852

4953
private final ApplicationContext context;
5054

51-
/**
52-
* @param context
53-
*/
5455
InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
5556
this.context = context;
5657
}
@@ -68,6 +69,7 @@ class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigu
6869
public void configure(AuthenticationManagerBuilder auth) {
6970
String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
7071
.getBeanNamesForType(UserDetailsService.class);
72+
7173
if (auth.isConfigured()) {
7274
if (beanNames.length > 0) {
7375
this.logger.warn("Global AuthenticationManager configured with an AuthenticationProvider bean. "
@@ -83,18 +85,27 @@ public void configure(AuthenticationManagerBuilder auth) {
8385
if (beanNames.length == 0) {
8486
return;
8587
}
86-
else if (beanNames.length > 1) {
87-
this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. "
88-
+ "Global Authentication Manager will not use a UserDetailsService for username/password login. "
89-
+ "Consider publishing a single UserDetailsService bean.", beanNames.length,
90-
Arrays.toString(beanNames)));
88+
89+
// Try to resolve a single candidate using the container's rules (@Primary,
90+
// etc.)
91+
UserDetailsService userDetailsService = getAutowireCandidateOrNull(UserDetailsService.class);
92+
93+
// If ambiguous or otherwise not resolvable, keep the warn-and-skip behavior
94+
if (userDetailsService == null) {
95+
if (beanNames.length > 1) {
96+
this.logger.warn(LogMessage.format("Found %s UserDetailsService beans, with names %s. "
97+
+ "Global Authentication Manager will not use a UserDetailsService for username/password login. "
98+
+ "Consider publishing a single (or primary) UserDetailsService bean.", beanNames.length,
99+
Arrays.toString(beanNames)));
100+
}
91101
return;
92102
}
93-
UserDetailsService userDetailsService = InitializeUserDetailsBeanManagerConfigurer.this.context
94-
.getBean(beanNames[0], UserDetailsService.class);
95-
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
96-
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
97-
CompromisedPasswordChecker passwordChecker = getBeanOrNull(CompromisedPasswordChecker.class);
103+
104+
PasswordEncoder passwordEncoder = getBeanIfUnique(PasswordEncoder.class);
105+
// Also resolve UDPS via container so @Primary is honored
106+
UserDetailsPasswordService passwordManager = getAutowireCandidateOrNull(UserDetailsPasswordService.class);
107+
CompromisedPasswordChecker passwordChecker = getBeanIfUnique(CompromisedPasswordChecker.class);
108+
98109
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
99110
if (passwordEncoder != null) {
100111
provider.setPasswordEncoder(passwordEncoder);
@@ -107,18 +118,47 @@ else if (beanNames.length > 1) {
107118
}
108119
provider.afterPropertiesSet();
109120
auth.authenticationProvider(provider);
121+
122+
String selectedName = resolveBeanName(beanNames, userDetailsService);
110123
this.logger.info(LogMessage.format(
111-
"Global AuthenticationManager configured with UserDetailsService bean with name %s", beanNames[0]));
124+
"Global AuthenticationManager configured with UserDetailsService bean with name %s", selectedName));
112125
}
113126

114127
/**
115-
* @return a bean of the requested class if there's just a single registered
116-
* component, null otherwise.
128+
* Resolve a single autowire candidate for the given type (honors
129+
* {@code @Primary}). Returns {@code null} if ambiguous or not present.
117130
*/
118-
private <T> T getBeanOrNull(Class<T> type) {
131+
private <T> T getAutowireCandidateOrNull(Class<T> type) {
132+
try {
133+
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfAvailable();
134+
}
135+
catch (BeansException ex) {
136+
return null;
137+
}
138+
}
139+
140+
/**
141+
* Return a bean of the requested class if there's exactly one registered
142+
* component; {@code null} otherwise.
143+
*/
144+
private <T> T getBeanIfUnique(Class<T> type) {
119145
return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique();
120146
}
121147

148+
private String resolveBeanName(String[] candidates, Object instance) {
149+
for (String name : candidates) {
150+
try {
151+
Object bean = InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(name);
152+
if (bean == instance) {
153+
return name;
154+
}
155+
}
156+
catch (BeansException ignored) {
157+
}
158+
}
159+
return instance.getClass().getName();
160+
}
161+
122162
}
123163

124164
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.authentication.configuration;
18+
19+
import java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.beans.factory.ObjectProvider;
24+
import org.springframework.context.ApplicationContext;
25+
import org.springframework.security.authentication.AuthenticationManager;
26+
import org.springframework.security.authentication.ProviderManager;
27+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
28+
import org.springframework.security.authentication.password.CompromisedPasswordChecker;
29+
import org.springframework.security.config.ObjectPostProcessor;
30+
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
31+
import org.springframework.security.core.userdetails.User;
32+
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
33+
import org.springframework.security.core.userdetails.UserDetailsService;
34+
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
35+
import org.springframework.security.crypto.password.PasswordEncoder;
36+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
37+
38+
import static org.assertj.core.api.Assertions.assertThat;
39+
import static org.mockito.BDDMockito.given;
40+
import static org.mockito.Mockito.mock;
41+
42+
class InitializeUserDetailsBeanManagerConfigurerTests {
43+
44+
private static ObjectPostProcessor<Object> opp() {
45+
return new ObjectPostProcessor<>() {
46+
@Override
47+
public <O> O postProcess(O object) {
48+
return object;
49+
}
50+
};
51+
}
52+
53+
@SuppressWarnings("unchecked")
54+
@Test
55+
void whenMultipleUdsAndOneResolvableCandidate_thenPrimaryIsAutoWired() throws Exception {
56+
ApplicationContext ctx = mock(ApplicationContext.class);
57+
given(ctx.getBeanNamesForType(UserDetailsService.class)).willReturn(new String[] { "udsA", "udsB" });
58+
59+
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
60+
61+
InMemoryUserDetailsManager primary = new InMemoryUserDetailsManager(
62+
User.withUsername("alice").passwordEncoder(encoder::encode).password("pw").roles("USER").build());
63+
InMemoryUserDetailsManager secondary = new InMemoryUserDetailsManager();
64+
65+
ObjectProvider<UserDetailsService> udsProvider = (ObjectProvider<UserDetailsService>) mock(
66+
ObjectProvider.class);
67+
given(ctx.getBeanProvider(UserDetailsService.class)).willReturn(udsProvider);
68+
given(udsProvider.getIfAvailable()).willReturn(primary); // container picks single
69+
// candidate
70+
71+
// resolveBeanName(..) path
72+
given(ctx.getBean("udsA")).willReturn(secondary);
73+
given(ctx.getBean("udsB")).willReturn(primary);
74+
75+
ObjectProvider<PasswordEncoder> peProvider = (ObjectProvider<PasswordEncoder>) mock(ObjectProvider.class);
76+
given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider);
77+
given(peProvider.getIfUnique()).willReturn(encoder);
78+
79+
// Stub optional providers to avoid NPEs
80+
ObjectProvider<UserDetailsPasswordService> udpsProvider = (ObjectProvider<UserDetailsPasswordService>) mock(
81+
ObjectProvider.class);
82+
given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider);
83+
given(udpsProvider.getIfAvailable()).willReturn(null);
84+
85+
ObjectProvider<CompromisedPasswordChecker> cpcProvider = (ObjectProvider<CompromisedPasswordChecker>) mock(
86+
ObjectProvider.class);
87+
given(ctx.getBeanProvider(CompromisedPasswordChecker.class)).willReturn(cpcProvider);
88+
given(cpcProvider.getIfUnique()).willReturn(null);
89+
90+
AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder(opp());
91+
new InitializeUserDetailsBeanManagerConfigurer(ctx).new InitializeUserDetailsManagerConfigurer()
92+
.configure(builder);
93+
94+
AuthenticationManager manager = builder.build();
95+
96+
// DaoAuthenticationProvider registered
97+
assertThat(manager).isInstanceOf(ProviderManager.class);
98+
List<?> providers = ((ProviderManager) manager).getProviders();
99+
assertThat(providers)
100+
.anySatisfy((p) -> assertThat(p.getClass().getSimpleName()).isEqualTo("DaoAuthenticationProvider"));
101+
102+
// Auth works with the primary UDS + encoder
103+
var auth = manager.authenticate(new UsernamePasswordAuthenticationToken("alice", "pw"));
104+
assertThat(auth.isAuthenticated()).isTrue();
105+
}
106+
107+
@SuppressWarnings("unchecked")
108+
@Test
109+
void whenMultipleUdsAndNoSingleCandidate_thenSkipAutoWiring() throws Exception {
110+
ApplicationContext ctx = mock(ApplicationContext.class);
111+
given(ctx.getBeanNamesForType(UserDetailsService.class)).willReturn(new String[] { "udsA", "udsB" });
112+
113+
ObjectProvider<UserDetailsService> udsProvider = (ObjectProvider<UserDetailsService>) mock(
114+
ObjectProvider.class);
115+
given(ctx.getBeanProvider(UserDetailsService.class)).willReturn(udsProvider);
116+
given(udsProvider.getIfAvailable()).willReturn(null); // ambiguous → no single
117+
// candidate
118+
119+
// Also stub other providers to null
120+
ObjectProvider<PasswordEncoder> peProvider = (ObjectProvider<PasswordEncoder>) mock(ObjectProvider.class);
121+
given(ctx.getBeanProvider(PasswordEncoder.class)).willReturn(peProvider);
122+
given(peProvider.getIfUnique()).willReturn(null);
123+
124+
ObjectProvider<UserDetailsPasswordService> udpsProvider = (ObjectProvider<UserDetailsPasswordService>) mock(
125+
ObjectProvider.class);
126+
given(ctx.getBeanProvider(UserDetailsPasswordService.class)).willReturn(udpsProvider);
127+
given(udpsProvider.getIfAvailable()).willReturn(null);
128+
129+
ObjectProvider<CompromisedPasswordChecker> cpcProvider = (ObjectProvider<CompromisedPasswordChecker>) mock(
130+
ObjectProvider.class);
131+
given(ctx.getBeanProvider(CompromisedPasswordChecker.class)).willReturn(cpcProvider);
132+
given(cpcProvider.getIfUnique()).willReturn(null);
133+
134+
AuthenticationManagerBuilder builder = new AuthenticationManagerBuilder(opp());
135+
new InitializeUserDetailsBeanManagerConfigurer(ctx).new InitializeUserDetailsManagerConfigurer()
136+
.configure(builder);
137+
138+
AuthenticationManager manager = builder.build();
139+
140+
// Success condition: nothing auto-registered.
141+
if (manager == null) {
142+
assertThat(manager).isNull();
143+
}
144+
else if (manager instanceof ProviderManager pm) {
145+
assertThat(pm.getProviders())
146+
.noneMatch((p) -> p.getClass().getSimpleName().equals("DaoAuthenticationProvider"));
147+
}
148+
else {
149+
assertThat(manager.getClass().getSimpleName()).isNotEqualTo("ProviderManager");
150+
}
151+
}
152+
153+
}

0 commit comments

Comments
 (0)