Skip to content

Commit e3ad551

Browse files
committed
Backport SubjectX500PrincipalExtractor
This commit backports SubjectX500PrincipalExtractor so as to provide folks moving from 6.x to 7.x a migration path from SubjectDnX509PrincipalExtractor. Issue gh-16980 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
1 parent cf06871 commit e3ad551

33 files changed

Lines changed: 1664 additions & 127 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
*
7575
* @author Rob Winch
7676
* @author Ngoc Nhan
77+
* @author Max Batischev
7778
* @since 3.2
7879
*/
7980
public final class X509Configurer<H extends HttpSecurityBuilder<H>>
@@ -161,7 +162,10 @@ public X509Configurer<H> authenticationUserDetailsService(
161162
* @param subjectPrincipalRegex the regex to extract the user principal from the
162163
* certificate (i.e. "CN=(.*?)(?:,|$)").
163164
* @return the {@link X509Configurer} for further customizations
165+
* @deprecated Please use {@link #x509PrincipalExtractor(X509PrincipalExtractor)}
166+
* instead
164167
*/
168+
@Deprecated
165169
public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
166170
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
167171
principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);

config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,11 +521,23 @@ void createX509Filter(BeanReference authManager,
521521
filterBuilder.addPropertyValue("authenticationManager", authManager);
522522
filterBuilder.addPropertyValue("securityContextHolderStrategy",
523523
authenticationFilterSecurityContextHolderStrategyRef);
524-
String regex = x509Elt.getAttribute("subject-principal-regex");
525-
if (StringUtils.hasText(regex)) {
524+
String principalExtractorRef = x509Elt.getAttribute("principal-extractor-ref");
525+
String subjectPrincipalRegex = x509Elt.getAttribute("subject-principal-regex");
526+
boolean hasPrincipalExtractorRef = StringUtils.hasText(principalExtractorRef);
527+
boolean hasSubjectPrincipalRegex = StringUtils.hasText(subjectPrincipalRegex);
528+
if (hasPrincipalExtractorRef && hasSubjectPrincipalRegex) {
529+
this.pc.getReaderContext()
530+
.error("The attribute 'principal-extractor-ref' cannot be used together with the 'subject-principal-regex' attribute within <"
531+
+ Elements.X509 + ">", this.pc.extractSource(x509Elt));
532+
}
533+
if (hasPrincipalExtractorRef) {
534+
RuntimeBeanReference principalExtractor = new RuntimeBeanReference(principalExtractorRef);
535+
filterBuilder.addPropertyValue("principalExtractor", principalExtractor);
536+
}
537+
if (hasSubjectPrincipalRegex) {
526538
BeanDefinitionBuilder extractor = BeanDefinitionBuilder
527539
.rootBeanDefinition(SubjectDnX509PrincipalExtractor.class);
528-
extractor.addPropertyValue("subjectDnRegex", regex);
540+
extractor.addPropertyValue("subjectDnRegex", subjectPrincipalRegex);
529541
filterBuilder.addPropertyValue("principalExtractor", extractor.getBeanDefinition());
530542
}
531543
injectAuthenticationDetailsSource(x509Elt, filterBuilder);

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
120120
import org.springframework.security.web.PortMapper;
121121
import org.springframework.security.web.authentication.logout.LogoutHandler;
122-
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
122+
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
123123
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
124124
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
125125
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
@@ -943,8 +943,8 @@ public ServerHttpSecurity formLogin(Customizer<FormLoginSpec> formLoginCustomize
943943
* }
944944
* </pre>
945945
*
946-
* Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor}
947-
* will be used. If authenticationManager is not specified,
946+
* Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will
947+
* be used. If authenticationManager is not specified,
948948
* {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
949949
* @return the {@link X509Spec} to customize
950950
* @since 5.2
@@ -978,8 +978,8 @@ public X509Spec x509() {
978978
* }
979979
* </pre>
980980
*
981-
* Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor}
982-
* will be used. If authenticationManager is not specified,
981+
* Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will
982+
* be used. If authenticationManager is not specified,
983983
* {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
984984
* @param x509Customizer the {@link Customizer} to provide more options for the
985985
* {@link X509Spec}
@@ -4180,7 +4180,7 @@ private X509PrincipalExtractor getPrincipalExtractor() {
41804180
if (this.principalExtractor != null) {
41814181
return this.principalExtractor;
41824182
}
4183-
return new SubjectDnX509PrincipalExtractor();
4183+
return new SubjectX500PrincipalExtractor();
41844184
}
41854185

41864186
private ReactiveAuthenticationManager getAuthenticationManager() {

config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,9 @@ x509.attlist &=
10501050
x509.attlist &=
10511051
## Reference to an AuthenticationDetailsSource which will be used by the authentication filter
10521052
attribute authentication-details-source-ref {xsd:token}?
1053+
x509.attlist &=
1054+
## Reference to an X509PrincipalExtractor which will be used by the authentication filter
1055+
attribute principal-extractor-ref {xsd:token}?
10531056

10541057
jee =
10551058
## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication.

config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2911,6 +2911,12 @@
29112911
</xs:documentation>
29122912
</xs:annotation>
29132913
</xs:attribute>
2914+
<xs:attribute name="principal-extractor-ref" type="xs:token">
2915+
<xs:annotation>
2916+
<xs:documentation>Reference to an X509PrincipalExtractor which will be used by the authentication filter
2917+
</xs:documentation>
2918+
</xs:annotation>
2919+
</xs:attribute>
29142920
</xs:attributeGroup>
29152921
<xs:element name="jee">
29162922
<xs:annotation>

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.SubjectX500PrincipalExtractor;
4647
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
48+
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
4749
import org.springframework.test.web.servlet.MockMvc;
4850

4951
import static org.assertj.core.api.Assertions.assertThat;
@@ -123,6 +125,28 @@ public void x509WhenSubjectPrincipalRegexInLambdaThenUsesRegexToExtractPrincipal
123125
// @formatter:on
124126
}
125127

128+
@Test
129+
public void x509WhenSubjectX500PrincipalExtractor() throws Exception {
130+
this.spring.register(SubjectX500PrincipalExtractorConfig.class).autowire();
131+
X509Certificate certificate = loadCert("rod.cer");
132+
// @formatter:off
133+
this.mvc.perform(get("/").with(x509(certificate)))
134+
.andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull())
135+
.andExpect(authenticated().withUsername("rod"));
136+
// @formatter:on
137+
}
138+
139+
@Test
140+
public void x509WhenSubjectX500PrincipalExtractorBean() throws Exception {
141+
this.spring.register(SubjectX500PrincipalExtractorEmailConfig.class).autowire();
142+
X509Certificate certificate = X509TestUtils.buildTestCertificate();
143+
// @formatter:off
144+
this.mvc.perform(get("/").with(x509(certificate)))
145+
.andExpect((result) -> assertThat(result.getRequest().getSession(false)).isNull())
146+
.andExpect(authenticated().withUsername("luke@monkeymachine"));
147+
// @formatter:on
148+
}
149+
126150
@Test
127151
public void x509WhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception {
128152
this.spring.register(UserDetailsServiceBeanConfig.class).autowire();
@@ -277,6 +301,62 @@ UserDetailsService userDetailsService() {
277301

278302
}
279303

304+
@Configuration
305+
@EnableWebSecurity
306+
static class SubjectX500PrincipalExtractorConfig {
307+
308+
@Bean
309+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
310+
// @formatter:off
311+
http
312+
.x509((x509) -> x509
313+
.x509PrincipalExtractor(new SubjectX500PrincipalExtractor())
314+
);
315+
// @formatter:on
316+
return http.build();
317+
}
318+
319+
@Bean
320+
UserDetailsService userDetailsService() {
321+
UserDetails user = User.withDefaultPasswordEncoder()
322+
.username("rod")
323+
.password("password")
324+
.roles("USER", "ADMIN")
325+
.build();
326+
return new InMemoryUserDetailsManager(user);
327+
}
328+
329+
}
330+
331+
@Configuration
332+
@EnableWebSecurity
333+
static class SubjectX500PrincipalExtractorEmailConfig {
334+
335+
@Bean
336+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
337+
SubjectX500PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
338+
principalExtractor.setExtractPrincipalNameFromEmail(true);
339+
// @formatter:off
340+
http
341+
.x509((x509) -> x509
342+
.x509PrincipalExtractor(principalExtractor)
343+
);
344+
// @formatter:on
345+
return http.build();
346+
}
347+
348+
@Bean
349+
UserDetailsService userDetailsService() {
350+
UserDetails user = User.withDefaultPasswordEncoder()
351+
.username("luke@monkeymachine")
352+
.password("password")
353+
.roles("USER", "ADMIN")
354+
.build();
355+
return new InMemoryUserDetailsManager(user);
356+
}
357+
358+
}
359+
280360
@Configuration
281361
@EnableWebSecurity
282362
static class UserDetailsServiceBeanConfig {

config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.OutputStream;
2222
import java.security.AccessController;
2323
import java.security.Principal;
24+
import java.security.cert.X509Certificate;
2425
import java.util.Arrays;
2526
import java.util.Collection;
2627
import java.util.HashSet;
@@ -91,6 +92,7 @@
9192
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
9293
import org.springframework.security.web.authentication.logout.LogoutFilter;
9394
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
95+
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
9496
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
9597
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
9698
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
@@ -398,6 +400,27 @@ public void configureWhenUsingX509ThenAddsX509FilterCorrectly() {
398400
.containsSubsequence(CsrfFilter.class, X509AuthenticationFilter.class, ExceptionTranslationFilter.class);
399401
}
400402

403+
@Test
404+
public void getWhenUsingX509PrincipalExtractorRef() throws Exception {
405+
this.spring.configLocations(xml("X509PrincipalExtractorRef")).autowire();
406+
X509Certificate certificate = X509TestUtils.buildTestCertificate();
407+
RequestPostProcessor x509 = x509(certificate);
408+
// @formatter:off
409+
this.mvc.perform(get("/protected").with(x509))
410+
.andExpect(status().isOk());
411+
// @formatter:on
412+
}
413+
414+
@Test
415+
public void getWhenUsingX509PrincipalExtractorRefAndSubjectPrincipalRegex() throws Exception {
416+
String xmlResourceName = "X509PrincipalExtractorRefAndSubjectPrincipalRegex";
417+
// @formatter:off
418+
assertThatExceptionOfType(BeanDefinitionParsingException.class)
419+
.isThrownBy(() -> this.spring.configLocations(xml(xmlResourceName)).autowire())
420+
.withMessage("Configuration problem: The attribute 'principal-extractor-ref' cannot be used together with the 'subject-principal-regex' attribute within <x509>\n" + "Offending resource: class path resource [org/springframework/security/config/http/MiscHttpConfigTests-X509PrincipalExtractorRefAndSubjectPrincipalRegex.xml]");
421+
// @formatter:on
422+
}
423+
401424
@Test
402425
public void getWhenUsingX509AndPropertyPlaceholderThenSubjectPrincipalRegexIsConfigured() throws Exception {
403426
System.setProperty("subject_principal_regex", "OU=(.*?)(?:,|$)");
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2018 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:p="http://www.springframework.org/schema/p"
20+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21+
xmlns="http://www.springframework.org/schema/security"
22+
xsi:schemaLocation="
23+
http://www.springframework.org/schema/security
24+
https://www.springframework.org/schema/security/spring-security.xsd
25+
http://www.springframework.org/schema/beans
26+
https://www.springframework.org/schema/beans/spring-beans.xsd">
27+
28+
<http>
29+
<x509 principal-extractor-ref="principalExtractor"/>
30+
<intercept-url pattern="/**" access="authenticated"/>
31+
</http>
32+
33+
<user-service id="us">
34+
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
35+
</user-service>
36+
37+
<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
38+
p:extractPrincipalNameFromEmail="true"/>
39+
40+
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
41+
</b:beans>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2018 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:p="http://www.springframework.org/schema/p"
20+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
21+
xmlns="http://www.springframework.org/schema/security"
22+
xsi:schemaLocation="
23+
http://www.springframework.org/schema/security
24+
https://www.springframework.org/schema/security/spring-security.xsd
25+
http://www.springframework.org/schema/beans
26+
https://www.springframework.org/schema/beans/spring-beans.xsd">
27+
28+
<http>
29+
<x509 principal-extractor-ref="principalExtractor" subject-principal-regex="(.*)"/>
30+
<intercept-url pattern="/**" access="authenticated"/>
31+
</http>
32+
33+
<user-service id="us">
34+
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
35+
</user-service>
36+
37+
<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
38+
p:extractPrincipalNameFromEmail="true"/>
39+
40+
<b:import resource="MiscHttpConfigTests-controllers.xml"/>
41+
</b:beans>

docs/antora.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ asciidoc:
1818
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
1919
include-java: 'example$docs-src/test/java/org/springframework/security/docs'
2020
include-kotlin: 'example$docs-src/test/kotlin/org/springframework/security/kt/docs'
21+
include-xml: 'example$docs-src/test/resources/org/springframework/security/docs'

0 commit comments

Comments
 (0)