Skip to content

Commit b4d4f9f

Browse files
Add support setting X509PrincipalExtractor as bean
Closes spring-projectsgh-17170 Signed-off-by: Max Batischev <[email protected]>
1 parent 52394c1 commit b4d4f9f

File tree

2 files changed

+84
-8
lines changed

2 files changed

+84
-8
lines changed

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -36,6 +36,8 @@
3636
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
3737
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
3838
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
39+
import org.springframework.util.Assert;
40+
import org.springframework.util.StringUtils;
3941

4042
/**
4143
* Adds X509 based pre authentication to an application. Since validating the certificate
@@ -74,6 +76,7 @@
7476
*
7577
* @author Rob Winch
7678
* @author Ngoc Nhan
79+
* @author Max Batischev
7780
* @since 3.2
7881
*/
7982
public final class X509Configurer<H extends HttpSecurityBuilder<H>>
@@ -87,6 +90,8 @@ public final class X509Configurer<H extends HttpSecurityBuilder<H>>
8790

8891
private AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource;
8992

93+
private String subjectPrincipalRegex;
94+
9095
/**
9196
* Creates a new instance
9297
*
@@ -163,9 +168,8 @@ public X509Configurer<H> authenticationUserDetailsService(
163168
* @return the {@link X509Configurer} for further customizations
164169
*/
165170
public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
166-
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
167-
principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);
168-
this.x509PrincipalExtractor = principalExtractor;
171+
Assert.hasText(subjectPrincipalRegex, "subjectPrincipalRegex cannot be null or empty");
172+
this.subjectPrincipalRegex = subjectPrincipalRegex;
169173
return this;
170174
}
171175

@@ -187,9 +191,7 @@ private X509AuthenticationFilter getFilter(AuthenticationManager authenticationM
187191
if (this.x509AuthenticationFilter == null) {
188192
this.x509AuthenticationFilter = new X509AuthenticationFilter();
189193
this.x509AuthenticationFilter.setAuthenticationManager(authenticationManager);
190-
if (this.x509PrincipalExtractor != null) {
191-
this.x509AuthenticationFilter.setPrincipalExtractor(this.x509PrincipalExtractor);
192-
}
194+
this.x509AuthenticationFilter.setPrincipalExtractor(getX509PrincipalExtractor(http));
193195
if (this.authenticationDetailsSource != null) {
194196
this.x509AuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
195197
}
@@ -209,6 +211,22 @@ private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> ge
209211
return this.authenticationUserDetailsService;
210212
}
211213

214+
private X509PrincipalExtractor getX509PrincipalExtractor(H http) {
215+
if (this.x509PrincipalExtractor != null) {
216+
return this.x509PrincipalExtractor;
217+
}
218+
X509PrincipalExtractor extractor = getSharedOrBean(http, X509PrincipalExtractor.class);
219+
if (extractor != null) {
220+
return extractor;
221+
}
222+
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
223+
if (StringUtils.hasText(this.subjectPrincipalRegex)) {
224+
principalExtractor.setSubjectDnRegex(this.subjectPrincipalRegex);
225+
}
226+
this.x509PrincipalExtractor = principalExtractor;
227+
return this.x509PrincipalExtractor;
228+
}
229+
212230
private <C> C getSharedOrBean(H http, Class<C> type) {
213231
C shared = http.getSharedObject(type);
214232
if (shared != null) {

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -43,7 +43,9 @@
4343
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
4444
import org.springframework.security.web.SecurityFilterChain;
4545
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
46+
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
4647
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
48+
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
4749
import org.springframework.test.web.servlet.MockMvc;
4850

4951
import static org.assertj.core.api.Assertions.assertThat;
@@ -155,6 +157,19 @@ public void x509WhenStatelessSessionManagementThenDoesNotCreateSession() throws
155157
// @formatter:on
156158
}
157159

160+
@Test
161+
public void x509WhenConfiguredX509PrincipalExtractorAsBeanThenUsesCustomExtractor() throws Exception {
162+
this.spring.register(X509PrincipalExtractorBeanConfig.class).autowire();
163+
X509Certificate certificate = loadCert("rod.cer");
164+
// @formatter:off
165+
this.mvc.perform(get("/").with(x509(certificate)))
166+
.andExpect(authenticated().withUsername("rod"));
167+
X509PrincipalExtractor extractor = this.spring.getContext().getBean(
168+
X509PrincipalExtractorBeanConfig.CustomX509PrincipalExtractor.class);
169+
verify(extractor).extractPrincipal(any(X509Certificate.class));
170+
// @formatter:on
171+
}
172+
158173
private <T extends Certificate> T loadCert(String location) {
159174
try (InputStream is = new ClassPathResource(location).getInputStream()) {
160175
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
@@ -360,4 +375,47 @@ UserDetailsService userDetailsService() {
360375

361376
}
362377

378+
@Configuration
379+
@EnableWebSecurity
380+
static class X509PrincipalExtractorBeanConfig {
381+
382+
private final CustomX509PrincipalExtractor extractor = spy(CustomX509PrincipalExtractor.class);
383+
384+
@Bean
385+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
386+
// @formatter:off
387+
http
388+
.x509(withDefaults());
389+
// @formatter:on
390+
return http.build();
391+
}
392+
393+
@Bean
394+
X509PrincipalExtractor x509PrincipalExtractor() {
395+
return this.extractor;
396+
}
397+
398+
@Bean
399+
UserDetailsService userDetailsService() {
400+
UserDetails user = User.withDefaultPasswordEncoder()
401+
.username("rod")
402+
.password("password")
403+
.roles("USER", "ADMIN")
404+
.build();
405+
return new InMemoryUserDetailsManager(user);
406+
}
407+
408+
public static final class CustomX509PrincipalExtractor implements X509PrincipalExtractor {
409+
410+
private final X509PrincipalExtractor extractor = new SubjectDnX509PrincipalExtractor();
411+
412+
@Override
413+
public Object extractPrincipal(X509Certificate cert) {
414+
return this.extractor.extractPrincipal(cert);
415+
}
416+
417+
}
418+
419+
}
420+
363421
}

0 commit comments

Comments
 (0)