From 88ed4a5ccfa312fcf8244c82ffbe727935738d84 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:22:20 -0500 Subject: [PATCH] Use principalExtractor reference instead of properties --- .../web/configurers/X509Configurer.java | 27 +--- .../http/AuthenticationConfigBuilder.java | 16 +-- .../security/config/annotation/web/X509Dsl.kt | 1 + .../security/config/spring-security-7.0.rnc | 3 - .../security/config/spring-security-7.0.xsd | 6 - .../web/configurers/X509ConfigurerTests.java | 119 ++++++++++++------ config/src/test/resources/max.cer | Bin 973 -> 0 bytes .../servlet/appendix/namespace/http.adoc | 3 - 8 files changed, 85 insertions(+), 90 deletions(-) delete mode 100644 config/src/test/resources/max.cer 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 b9b766a14d..d6241a0fbe 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 @@ -33,7 +33,6 @@ import org.springframework.security.web.authentication.preauth.PreAuthenticatedA import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails; import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor; -import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor; 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; @@ -75,7 +74,6 @@ import org.springframework.security.web.context.RequestAttributeSecurityContextR * * @author Rob Winch * @author Ngoc Nhan - * @author Max Batischev * @since 3.2 */ public final class X509Configurer> @@ -163,38 +161,17 @@ public final class X509Configurer> * @param subjectPrincipalRegex the regex to extract the user principal from the * certificate (i.e. "CN=(.*?)(?:,|$)"). * @return the {@link X509Configurer} for further customizations - * @deprecated Please use {{@link #extractPrincipalNameFromEmail(boolean)}} instead + * @deprecated Please use {{@link #x509PrincipalExtractor(X509PrincipalExtractor)} + * instead */ @Deprecated public X509Configurer subjectPrincipalRegex(String subjectPrincipalRegex) { - if (this.x509PrincipalExtractor instanceof SubjectX500PrincipalExtractor) { - throw new IllegalStateException( - "Cannot use subjectPrincipalRegex and extractPrincipalNameFromEmail together. " - + "Please use one or the other."); - } SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); principalExtractor.setSubjectDnRegex(subjectPrincipalRegex); this.x509PrincipalExtractor = principalExtractor; return this; } - /** - * If true then DN will be extracted from EMAIlADDRESS, defaults to {@code false} - * @param extractPrincipalNameFromEmail whether to extract DN from EMAIlADDRESS - * @since 7.0 - */ - public X509Configurer extractPrincipalNameFromEmail(boolean extractPrincipalNameFromEmail) { - if (this.x509PrincipalExtractor instanceof SubjectDnX509PrincipalExtractor) { - throw new IllegalStateException( - "Cannot use subjectPrincipalRegex and extractPrincipalNameFromEmail together. " - + "Please use one or the other."); - } - SubjectX500PrincipalExtractor extractor = new SubjectX500PrincipalExtractor(); - extractor.setExtractPrincipalNameFromEmail(extractPrincipalNameFromEmail); - this.x509PrincipalExtractor = extractor; - return this; - } - @Override public void init(H http) { PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider(); diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 9b6ee210ac..f997e5fa5e 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2025 the original author or authors. + * Copyright 2002-2023 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. @@ -57,7 +57,6 @@ import org.springframework.security.web.authentication.preauth.PreAuthenticatedG import org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource; import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor; -import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; @@ -523,25 +522,12 @@ final class AuthenticationConfigBuilder { filterBuilder.addPropertyValue("securityContextHolderStrategy", authenticationFilterSecurityContextHolderStrategyRef); String regex = x509Elt.getAttribute("subject-principal-regex"); - String extractPrincipalNameFromEmail = x509Elt.getAttribute("extract-principal-name-from-email"); - if (StringUtils.hasText(regex) && StringUtils.hasText(extractPrincipalNameFromEmail)) { - throw new IllegalStateException( - "Cannot use subjectPrincipalRegex and extractPrincipalNameFromEmail together. " - + "Please use one or the other."); - } if (StringUtils.hasText(regex)) { BeanDefinitionBuilder extractor = BeanDefinitionBuilder .rootBeanDefinition(SubjectDnX509PrincipalExtractor.class); extractor.addPropertyValue("subjectDnRegex", regex); filterBuilder.addPropertyValue("principalExtractor", extractor.getBeanDefinition()); } - if (StringUtils.hasText(extractPrincipalNameFromEmail)) { - BeanDefinitionBuilder extractor = BeanDefinitionBuilder - .rootBeanDefinition(SubjectX500PrincipalExtractor.class); - extractor.addPropertyValue("extractPrincipalNameFromEmail", - Boolean.parseBoolean(extractPrincipalNameFromEmail)); - filterBuilder.addPropertyValue("principalExtractor", extractor.getBeanDefinition()); - } injectAuthenticationDetailsSource(x509Elt, filterBuilder); filter = (RootBeanDefinition) filterBuilder.getBeanDefinition(); createPrauthEntryPoint(x509Elt); diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt index f36897604a..514ace50b3 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/X509Dsl.kt @@ -51,6 +51,7 @@ class X509Dsl { var authenticationDetailsSource: AuthenticationDetailsSource? = null var userDetailsService: UserDetailsService? = null var authenticationUserDetailsService: AuthenticationUserDetailsService? = null + @Deprecated("Use x509PrincipalExtractor instead") var subjectPrincipalRegex: String? = null diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc index 1236a2aec5..bbf8622dfe 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.rnc @@ -1053,9 +1053,6 @@ x509.attlist &= x509.attlist &= ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter attribute authentication-details-source-ref {xsd:token}? -x509.attlist &= - ## If true then DN will be extracted from EMAIlADDRESS - attribute extract-principal-name-from-email {xsd:token}? jee = ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd index ed82f3468b..2e3d6cf275 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-7.0.xsd @@ -2917,12 +2917,6 @@ - - - If true then DN will be extracted from EMAIlADDRESS - - - 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 38983691e1..63a3a963df 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-2025 the original author or authors. + * Copyright 2002-2023 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.core.userdetails.UserDetailsService; 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.SubjectX500PrincipalExtractor; import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; +import org.springframework.security.web.authentication.preauth.x509.X509TestUtils; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -123,16 +125,6 @@ public class X509ConfigurerTests { // @formatter:on } - @Test - public void x509WhenExtractPrincipalNameFromEmailIsTrueThenUsesEmailAddressToExtractPrincipal() throws Exception { - this.spring.register(EmailPrincipalConfig.class).autowire(); - X509Certificate certificate = loadCert("max.cer"); - // @formatter:off - this.mvc.perform(get("/").with(x509(certificate))) - .andExpect(authenticated().withUsername("maxbatischev@gmail.com")); - // @formatter:on - } - @Test public void x509WhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception { this.spring.register(UserDetailsServiceBeanConfig.class).autowire(); @@ -165,6 +157,28 @@ public class X509ConfigurerTests { // @formatter:on } + @Test + public void x509WhenSubjectX500PrincipalExtractor() throws Exception { + this.spring.register(SubjectX500PrincipalExtractorConfig.class).autowire(); + X509Certificate certificate = loadCert("rod.cer"); + // @formatter:off + this.mvc.perform(get("/").with(x509(certificate))) + .andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull()) + .andExpect(authenticated().withUsername("rod")); + // @formatter:on + } + + @Test + public void x509WhenSubjectX500PrincipalExtractorBean() throws Exception { + this.spring.register(SubjectX500PrincipalExtractorEmailConfig.class).autowire(); + X509Certificate certificate = X509TestUtils.buildTestCertificate(); + // @formatter:off + this.mvc.perform(get("/").with(x509(certificate))) + .andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull()) + .andExpect(authenticated().withUsername("luke@monkeymachine")); + // @formatter:on + } + private T loadCert(String location) { try (InputStream is = new ClassPathResource(location).getInputStream()) { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -287,33 +301,6 @@ public class X509ConfigurerTests { } - @Configuration - @EnableWebSecurity - static class EmailPrincipalConfig { - - @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // @formatter:off - http - .x509((x509) -> - x509.extractPrincipalNameFromEmail(true) - ); - // @formatter:on - return http.build(); - } - - @Bean - UserDetailsService userDetailsService() { - UserDetails user = User.withDefaultPasswordEncoder() - .username("maxbatischev@gmail.com") - .password("password") - .roles("USER", "ADMIN") - .build(); - return new InMemoryUserDetailsManager(user); - } - - } - @Configuration @EnableWebSecurity static class UserDetailsServiceBeanConfig { @@ -397,4 +384,60 @@ public class X509ConfigurerTests { } + @Configuration + @EnableWebSecurity + static class SubjectX500PrincipalExtractorConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .x509((x509) -> x509 + .x509PrincipalExtractor(new SubjectX500PrincipalExtractor()) + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user); + } + + } + + @Configuration + @EnableWebSecurity + static class SubjectX500PrincipalExtractorEmailConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor(); + principalExtractor.setExtractPrincipalNameFromEmail(true); + // @formatter:off + http + .x509((x509) -> x509 + .x509PrincipalExtractor(principalExtractor) + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("luke@monkeymachine") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user); + } + + } + } diff --git a/config/src/test/resources/max.cer b/config/src/test/resources/max.cer deleted file mode 100644 index bd79b1f096b29fb62a456f63a9ae7eeecc2fcdd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 973 zcmd6mNspUA5QXpjirmr27`(u-dNBsOfrbVHw=p(g%y@}e=hw&MJ z>DAlYvk)~c=NqQJuvQiZLBGT|iCGrDRv1Ik7SLM=if0pWP;tJO=aXsl3IeXIfS{J~ zM~`_zBF;_P+v7n{Phy~$Fbye&-V{6IGf@qValI!$!3}{_WNZ7j|Hy__1tBV&2mxax zoR&w7u$099EmNIq8YhD-p!$6?=5)9Ec!H)~9?beVysQsrblLA#w>_R$dsXSBb(uM= z3VTOTg^6r*37hX;TJon>{kJC+>B}&Cb4{(X%N*boAr7QS6kwKV!_`EvjZ$?pIk}c| zT&J@bB8`7Iei6tgBt^ZF@04ZxnVj)8NW*FEB2JaFF6RfP%CP~DqR)LywI)pOtp_){ z882ua8NsoX_rBC+#(TB&+6x;U`LpuFMZBD#trp$4`FDF8WYhVczX#6rR@w_k$VkMV zXiwfgt`8S`i={rgF1+Y;>~(ESHEj+BLjYzK667-U%E*BfeD}Po9Cihwb-<`eU|0MD{>8Ev>obZ35DEBt@|8v$+|9t+YvgGn;UU26X`W?S z_HcU$uL%48_X?2AMnI)aRIM3Wc*A_(C z@WyhQLK_!Ecja|3)@4*ohsHf^mbFCdIP#A-q?=i+b-A&c>z(nC$4PTb|i mn&q8==RqDeEScF}InkJSjJ57cO)-c~)w$B^IjKYbe%K$#hcR#f diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 812706ae1d..8979d5ad29 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -2229,9 +2229,6 @@ Defines a regular expression which will be used to extract the username from the Allows a specific `UserDetailsService` to be used with X.509 in the case where multiple instances are configured. If not set, an attempt will be made to locate a suitable instance automatically and use that. -[[nsa-x509-extract-principal-name-from-email]] -* **extract-principal-name-from-email** -If true then DN will be extracted from EMAIlADDRESS. [[nsa-filter-chain-map]] ==