Skip to content

Add Support Extracting DN From X500Principal #16984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -161,7 +161,10 @@ public X509Configurer<H> authenticationUserDetailsService(
* @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 #x509PrincipalExtractor(X509PrincipalExtractor)}
* instead
*/
@Deprecated
public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,11 +521,23 @@ void createX509Filter(BeanReference authManager,
filterBuilder.addPropertyValue("authenticationManager", authManager);
filterBuilder.addPropertyValue("securityContextHolderStrategy",
authenticationFilterSecurityContextHolderStrategyRef);
String regex = x509Elt.getAttribute("subject-principal-regex");
if (StringUtils.hasText(regex)) {
String principalExtractorRef = x509Elt.getAttribute("principal-extractor-ref");
String subjectPrincipalRegex = x509Elt.getAttribute("subject-principal-regex");
boolean hasPrincipalExtractorRef = StringUtils.hasText(principalExtractorRef);
boolean hasSubjectPrincipalRegex = StringUtils.hasText(subjectPrincipalRegex);
if (hasPrincipalExtractorRef && hasSubjectPrincipalRegex) {
this.pc.getReaderContext()
.error("The attribute 'principal-extractor-ref' cannot be used together with the 'subject-principal-regex' attribute within <"
+ Elements.X509 + ">", this.pc.extractSource(x509Elt));
}
if (hasPrincipalExtractorRef) {
RuntimeBeanReference principalExtractor = new RuntimeBeanReference(principalExtractorRef);
filterBuilder.addPropertyValue("principalExtractor", principalExtractor);
}
if (hasSubjectPrincipalRegex) {
BeanDefinitionBuilder extractor = BeanDefinitionBuilder
.rootBeanDefinition(SubjectDnX509PrincipalExtractor.class);
extractor.addPropertyValue("subjectDnRegex", regex);
extractor.addPropertyValue("subjectDnRegex", subjectPrincipalRegex);
filterBuilder.addPropertyValue("principalExtractor", extractor.getBeanDefinition());
}
injectAuthenticationDetailsSource(x509Elt, filterBuilder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.PortMapper;
import org.springframework.security.web.authentication.logout.LogoutHandler;
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.X509PrincipalExtractor;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
Expand Down Expand Up @@ -944,8 +944,8 @@ public ServerHttpSecurity formLogin(Customizer<FormLoginSpec> formLoginCustomize
* }
* </pre>
*
* Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor}
* will be used. If authenticationManager is not specified,
* Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will
* be used. If authenticationManager is not specified,
* {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
* @return the {@link X509Spec} to customize
* @since 5.2
Expand Down Expand Up @@ -979,8 +979,8 @@ public X509Spec x509() {
* }
* </pre>
*
* Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor}
* will be used. If authenticationManager is not specified,
* Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will
* be used. If authenticationManager is not specified,
* {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
* @param x509Customizer the {@link Customizer} to provide more options for the
* {@link X509Spec}
Expand Down Expand Up @@ -4181,7 +4181,7 @@ private X509PrincipalExtractor getPrincipalExtractor() {
if (this.principalExtractor != null) {
return this.principalExtractor;
}
return new SubjectDnX509PrincipalExtractor();
return new SubjectX500PrincipalExtractor();
}

private ReactiveAuthenticationManager getAuthenticationManager() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class X509Dsl {
var authenticationDetailsSource: AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails>? = null
var userDetailsService: UserDetailsService? = null
var authenticationUserDetailsService: AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>? = null
@Deprecated("Use x509PrincipalExtractor instead")
var subjectPrincipalRegex: String? = null


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,9 @@ 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 &=
## Reference to an X509PrincipalExtractor which will be used by the authentication filter
attribute principal-extractor-ref {xsd:token}?

jee =
## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2917,6 +2917,12 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="principal-extractor-ref" type="xs:token">
<xs:annotation>
<xs:documentation>Reference to an X509PrincipalExtractor which will be used by the authentication filter
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="jee">
<xs:annotation>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.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;
Expand Down Expand Up @@ -155,6 +157,28 @@ public void x509WhenStatelessSessionManagementThenDoesNotCreateSession() throws
// @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 extends Certificate> T loadCert(String location) {
try (InputStream is = new ClassPathResource(location).getInputStream()) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
Expand Down Expand Up @@ -360,4 +384,60 @@ UserDetailsService userDetailsService() {

}

@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);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.io.OutputStream;
import java.security.AccessController;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
Expand Down Expand Up @@ -91,6 +92,7 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509TestUtils;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
Expand Down Expand Up @@ -398,6 +400,27 @@ public void configureWhenUsingX509ThenAddsX509FilterCorrectly() {
.containsSubsequence(CsrfFilter.class, X509AuthenticationFilter.class, ExceptionTranslationFilter.class);
}

@Test
public void getWhenUsingX509PrincipalExtractorRef() throws Exception {
this.spring.configLocations(xml("X509PrincipalExtractorRef")).autowire();
X509Certificate certificate = X509TestUtils.buildTestCertificate();
RequestPostProcessor x509 = x509(certificate);
// @formatter:off
this.mvc.perform(get("/protected").with(x509))
.andExpect(status().isOk());
// @formatter:on
}

@Test
public void getWhenUsingX509PrincipalExtractorRefAndSubjectPrincipalRegex() throws Exception {
String xmlResourceName = "X509PrincipalExtractorRefAndSubjectPrincipalRegex";
// @formatter:off
assertThatExceptionOfType(BeanDefinitionParsingException.class)
.isThrownBy(() -> this.spring.configLocations(xml(xmlResourceName)).autowire())
.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]");
// @formatter:on
}

@Test
public void getWhenUsingX509AndPropertyPlaceholderThenSubjectPrincipalRegexIsConfigured() throws Exception {
System.setProperty("subject_principal_regex", "OU=(.*?)(?:,|$)");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 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.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<http>
<x509 principal-extractor-ref="principalExtractor"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>

<user-service id="us">
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
</user-service>

<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
p:extractPrincipalNameFromEmail="true"/>

<b:import resource="MiscHttpConfigTests-controllers.xml"/>
</b:beans>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 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.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<http>
<x509 principal-extractor-ref="principalExtractor" subject-principal-regex="(.*)"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>

<user-service id="us">
<user name="luke@monkeymachine" password="{noop}password" authorities="ROLE_USER"/>
</user-service>

<b:bean name="principalExtractor" class="org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor"
p:extractPrincipalNameFromEmail="true"/>

<b:import resource="MiscHttpConfigTests-controllers.xml"/>
</b:beans>
1 change: 1 addition & 0 deletions docs/antora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ asciidoc:
gh-url: "https://github.com/spring-projects/spring-security/tree/{gh-tag}"
include-java: 'example$docs-src/test/java/org/springframework/security/docs'
include-kotlin: 'example$docs-src/test/kotlin/org/springframework/security/kt/docs'
include-xml: 'example$docs-src/test/resources/org/springframework/security/docs'
Loading