Skip to content

Commit 5ac5edc

Browse files
committed
Detect UserDetailsService bean in X509 configuration
Closes gh-11174
1 parent d40c15e commit 5ac5edc

File tree

2 files changed

+111
-4
lines changed

2 files changed

+111
-4
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,15 +18,19 @@
1818

1919
import javax.servlet.http.HttpServletRequest;
2020

21+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
22+
import org.springframework.context.ApplicationContext;
2123
import org.springframework.security.authentication.AuthenticationDetailsSource;
2224
import org.springframework.security.authentication.AuthenticationManager;
2325
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2426
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
27+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
2528
import org.springframework.security.core.Authentication;
2629
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
2730
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
2831
import org.springframework.security.core.userdetails.UserDetailsService;
2932
import org.springframework.security.web.AuthenticationEntryPoint;
33+
import org.springframework.security.web.SecurityFilterChain;
3034
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
3135
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
3236
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
@@ -141,7 +145,9 @@ public X509Configurer<H> userDetailsService(UserDetailsService userDetailsServic
141145
/**
142146
* Specifies the {@link AuthenticationUserDetailsService} to use. If not specified,
143147
* the shared {@link UserDetailsService} will be used to create a
144-
* {@link UserDetailsByNameServiceWrapper}.
148+
* {@link UserDetailsByNameServiceWrapper}. If a {@link SecurityFilterChain} bean is
149+
* used instead of the {@link WebSecurityConfigurerAdapter}, then the
150+
* {@link UserDetailsService} bean will be used by default.
145151
* @param authenticationUserDetailsService the
146152
* {@link AuthenticationUserDetailsService} to use
147153
* @return the {@link X509Configurer} for further customizations
@@ -200,9 +206,30 @@ private X509AuthenticationFilter getFilter(AuthenticationManager authenticationM
200206
private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> getAuthenticationUserDetailsService(
201207
H http) {
202208
if (this.authenticationUserDetailsService == null) {
203-
userDetailsService(http.getSharedObject(UserDetailsService.class));
209+
userDetailsService(getSharedOrBean(http, UserDetailsService.class));
204210
}
205211
return this.authenticationUserDetailsService;
206212
}
207213

214+
private <C> C getSharedOrBean(H http, Class<C> type) {
215+
C shared = http.getSharedObject(type);
216+
if (shared != null) {
217+
return shared;
218+
}
219+
return getBeanOrNull(type);
220+
}
221+
222+
private <T> T getBeanOrNull(Class<T> type) {
223+
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
224+
if (context == null) {
225+
return null;
226+
}
227+
try {
228+
return context.getBean(type);
229+
}
230+
catch (NoSuchBeanDefinitionException ex) {
231+
return null;
232+
}
233+
}
234+
208235
}

config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,10 +34,15 @@
3434
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
3535
import org.springframework.security.config.test.SpringTestContext;
3636
import org.springframework.security.config.test.SpringTestContextExtension;
37+
import org.springframework.security.core.userdetails.User;
38+
import org.springframework.security.core.userdetails.UserDetailsService;
39+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
40+
import org.springframework.security.web.SecurityFilterChain;
3741
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3842
import org.springframework.test.web.servlet.MockMvc;
3943

4044
import static org.mockito.ArgumentMatchers.any;
45+
import static org.mockito.Mockito.mock;
4146
import static org.mockito.Mockito.spy;
4247
import static org.mockito.Mockito.verify;
4348
import static org.springframework.security.config.Customizer.withDefaults;
@@ -95,6 +100,26 @@ public void x509WhenSubjectPrincipalRegexInLambdaThenUsesRegexToExtractPrincipal
95100
// @formatter:on
96101
}
97102

103+
@Test
104+
public void x509WhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception {
105+
this.spring.register(UserDetailsServiceBeanConfig.class).autowire();
106+
X509Certificate certificate = loadCert("rod.cer");
107+
// @formatter:off
108+
this.mvc.perform(get("/").with(x509(certificate)))
109+
.andExpect(authenticated().withUsername("rod"));
110+
// @formatter:on
111+
}
112+
113+
@Test
114+
public void x509WhenUserDetailsServiceAndBeanConfiguredThenDoesNotUseBean() throws Exception {
115+
this.spring.register(UserDetailsServiceAndBeanConfig.class).autowire();
116+
X509Certificate certificate = loadCert("rod.cer");
117+
// @formatter:off
118+
this.mvc.perform(get("/").with(x509(certificate)))
119+
.andExpect(authenticated().withUsername("rod"));
120+
// @formatter:on
121+
}
122+
98123
private <T extends Certificate> T loadCert(String location) {
99124
try (InputStream is = new ClassPathResource(location).getInputStream()) {
100125
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
@@ -206,4 +231,59 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
206231

207232
}
208233

234+
@EnableWebSecurity
235+
static class UserDetailsServiceBeanConfig {
236+
237+
@Bean
238+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
239+
// @formatter:off
240+
http
241+
.x509(withDefaults());
242+
// @formatter:on
243+
return http.build();
244+
}
245+
246+
@Bean
247+
UserDetailsService userDetailsService() {
248+
// @formatter:off
249+
return new InMemoryUserDetailsManager(
250+
User.withDefaultPasswordEncoder()
251+
.username("rod")
252+
.password("password")
253+
.roles("USER", "ADMIN")
254+
.build()
255+
);
256+
// @formatter:on
257+
}
258+
259+
}
260+
261+
@EnableWebSecurity
262+
static class UserDetailsServiceAndBeanConfig {
263+
264+
@Bean
265+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
266+
// @formatter:off
267+
UserDetailsService customUserDetailsService = new InMemoryUserDetailsManager(
268+
User.withDefaultPasswordEncoder()
269+
.username("rod")
270+
.password("password")
271+
.roles("USER", "ADMIN")
272+
.build());
273+
http
274+
.x509((x509) -> x509
275+
.userDetailsService(customUserDetailsService)
276+
);
277+
// @formatter:on
278+
return http.build();
279+
}
280+
281+
@Bean
282+
UserDetailsService userDetailsService() {
283+
// @formatter:off
284+
return mock(UserDetailsService.class);
285+
}
286+
287+
}
288+
209289
}

0 commit comments

Comments
 (0)