From b4d4f9f79950be6582c39ea28bc598e6a7291ade Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Mon, 26 May 2025 12:50:17 +0300 Subject: [PATCH 1/2] Add support setting X509PrincipalExtractor as bean Closes gh-17170 Signed-off-by: Max Batischev --- .../web/configurers/X509Configurer.java | 32 +++++++--- .../web/configurers/X509ConfigurerTests.java | 60 ++++++++++++++++++- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index a3818e2a9ac..a4630eea123 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -36,6 +36,8 @@ import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Adds X509 based pre authentication to an application. Since validating the certificate @@ -74,6 +76,7 @@ * * @author Rob Winch * @author Ngoc Nhan + * @author Max Batischev * @since 3.2 */ public final class X509Configurer> @@ -87,6 +90,8 @@ public final class X509Configurer> private AuthenticationDetailsSource authenticationDetailsSource; + private String subjectPrincipalRegex; + /** * Creates a new instance * @@ -163,9 +168,8 @@ public X509Configurer authenticationUserDetailsService( * @return the {@link X509Configurer} for further customizations */ public X509Configurer subjectPrincipalRegex(String subjectPrincipalRegex) { - SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); - principalExtractor.setSubjectDnRegex(subjectPrincipalRegex); - this.x509PrincipalExtractor = principalExtractor; + Assert.hasText(subjectPrincipalRegex, "subjectPrincipalRegex cannot be null or empty"); + this.subjectPrincipalRegex = subjectPrincipalRegex; return this; } @@ -187,9 +191,7 @@ private X509AuthenticationFilter getFilter(AuthenticationManager authenticationM if (this.x509AuthenticationFilter == null) { this.x509AuthenticationFilter = new X509AuthenticationFilter(); this.x509AuthenticationFilter.setAuthenticationManager(authenticationManager); - if (this.x509PrincipalExtractor != null) { - this.x509AuthenticationFilter.setPrincipalExtractor(this.x509PrincipalExtractor); - } + this.x509AuthenticationFilter.setPrincipalExtractor(getX509PrincipalExtractor(http)); if (this.authenticationDetailsSource != null) { this.x509AuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource); } @@ -209,6 +211,22 @@ private AuthenticationUserDetailsService ge return this.authenticationUserDetailsService; } + private X509PrincipalExtractor getX509PrincipalExtractor(H http) { + if (this.x509PrincipalExtractor != null) { + return this.x509PrincipalExtractor; + } + X509PrincipalExtractor extractor = getSharedOrBean(http, X509PrincipalExtractor.class); + if (extractor != null) { + return extractor; + } + SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); + if (StringUtils.hasText(this.subjectPrincipalRegex)) { + principalExtractor.setSubjectDnRegex(this.subjectPrincipalRegex); + } + this.x509PrincipalExtractor = principalExtractor; + return this.x509PrincipalExtractor; + } + private C getSharedOrBean(H http, Class type) { C shared = http.getSharedObject(type); if (shared != null) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java index 206c0b7ecee..2a45f4c6750 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -43,7 +43,9 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -155,6 +157,19 @@ public void x509WhenStatelessSessionManagementThenDoesNotCreateSession() throws // @formatter:on } + @Test + public void x509WhenConfiguredX509PrincipalExtractorAsBeanThenUsesCustomExtractor() throws Exception { + this.spring.register(X509PrincipalExtractorBeanConfig.class).autowire(); + X509Certificate certificate = loadCert("rod.cer"); + // @formatter:off + this.mvc.perform(get("/").with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")); + X509PrincipalExtractor extractor = this.spring.getContext().getBean( + X509PrincipalExtractorBeanConfig.CustomX509PrincipalExtractor.class); + verify(extractor).extractPrincipal(any(X509Certificate.class)); + // @formatter:on + } + private T loadCert(String location) { try (InputStream is = new ClassPathResource(location).getInputStream()) { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -360,4 +375,47 @@ UserDetailsService userDetailsService() { } + @Configuration + @EnableWebSecurity + static class X509PrincipalExtractorBeanConfig { + + private final CustomX509PrincipalExtractor extractor = spy(CustomX509PrincipalExtractor.class); + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .x509(withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + X509PrincipalExtractor x509PrincipalExtractor() { + return this.extractor; + } + + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user); + } + + public static final class CustomX509PrincipalExtractor implements X509PrincipalExtractor { + + private final X509PrincipalExtractor extractor = new SubjectDnX509PrincipalExtractor(); + + @Override + public Object extractPrincipal(X509Certificate cert) { + return this.extractor.extractPrincipal(cert); + } + + } + + } + } From d83dafedb8805bc7c834b01ff4e30cbe7aee3a5a Mon Sep 17 00:00:00 2001 From: Max Batischev Date: Mon, 26 May 2025 12:52:50 +0300 Subject: [PATCH 2/2] Add assertions to X509Configurer Signed-off-by: Max Batischev --- .../config/annotation/web/configurers/X509Configurer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java index a4630eea123..0358dda6e13 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/X509Configurer.java @@ -108,6 +108,7 @@ public X509Configurer() { * @return the {@link X509Configurer} for further customizations */ public X509Configurer x509AuthenticationFilter(X509AuthenticationFilter x509AuthenticationFilter) { + Assert.notNull(x509AuthenticationFilter, "x509AuthenticationFilter cannot be null"); this.x509AuthenticationFilter = x509AuthenticationFilter; return this; } @@ -118,6 +119,7 @@ public X509Configurer x509AuthenticationFilter(X509AuthenticationFilter x509A * @return the {@link X509Configurer} to use */ public X509Configurer x509PrincipalExtractor(X509PrincipalExtractor x509PrincipalExtractor) { + Assert.notNull(x509PrincipalExtractor, "x509PrincipalExtractor cannot be null"); this.x509PrincipalExtractor = x509PrincipalExtractor; return this; } @@ -129,6 +131,7 @@ public X509Configurer x509PrincipalExtractor(X509PrincipalExtractor x509Princ */ public X509Configurer authenticationDetailsSource( AuthenticationDetailsSource authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); this.authenticationDetailsSource = authenticationDetailsSource; return this; } @@ -155,6 +158,7 @@ public X509Configurer userDetailsService(UserDetailsService userDetailsServic */ public X509Configurer authenticationUserDetailsService( AuthenticationUserDetailsService authenticationUserDetailsService) { + Assert.notNull(authenticationUserDetailsService, "authenticationUserDetailsService cannot be null"); this.authenticationUserDetailsService = authenticationUserDetailsService; return this; }