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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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 @@ -33,9 +33,11 @@
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;
import org.springframework.util.Assert;

/**
* Adds X509 based pre authentication to an application. Since validating the certificate
Expand Down Expand Up @@ -74,6 +76,7 @@
*
* @author Rob Winch
* @author Ngoc Nhan
* @author Max Batischev
* @since 3.2
*/
public final class X509Configurer<H extends HttpSecurityBuilder<H>>
Expand Down Expand Up @@ -103,6 +106,7 @@ public X509Configurer() {
* @return the {@link X509Configurer} for further customizations
*/
public X509Configurer<H> x509AuthenticationFilter(X509AuthenticationFilter x509AuthenticationFilter) {
Assert.notNull(x509AuthenticationFilter, "x509AuthenticationFilter cannot be null");
this.x509AuthenticationFilter = x509AuthenticationFilter;
return this;
}
Expand All @@ -113,6 +117,7 @@ public X509Configurer<H> x509AuthenticationFilter(X509AuthenticationFilter x509A
* @return the {@link X509Configurer} to use
*/
public X509Configurer<H> x509PrincipalExtractor(X509PrincipalExtractor x509PrincipalExtractor) {
Assert.notNull(x509PrincipalExtractor, "x509PrincipalExtractor cannot be null");
this.x509PrincipalExtractor = x509PrincipalExtractor;
return this;
}
Expand All @@ -124,6 +129,7 @@ public X509Configurer<H> x509PrincipalExtractor(X509PrincipalExtractor x509Princ
*/
public X509Configurer<H> authenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
return this;
}
Expand All @@ -150,6 +156,7 @@ public X509Configurer<H> userDetailsService(UserDetailsService userDetailsServic
*/
public X509Configurer<H> authenticationUserDetailsService(
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService) {
Assert.notNull(authenticationUserDetailsService, "authenticationUserDetailsService cannot be null");
this.authenticationUserDetailsService = authenticationUserDetailsService;
return this;
}
Expand All @@ -161,14 +168,38 @@ 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 #extractPrincipalNameFromEmail(boolean)}} instead
*/
@Deprecated
public X509Configurer<H> 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<H> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -57,6 +57,7 @@
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;
Expand Down Expand Up @@ -522,12 +523,25 @@ void createX509Filter(BeanReference authManager,
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);
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 @@ -943,8 +943,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 @@ -978,8 +978,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 @@ -4180,7 +4180,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 @@ -1050,6 +1050,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 &=
## 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2911,6 +2911,12 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="extract-principal-name-from-email" type="xs:token">
<xs:annotation>
<xs:documentation>If true then DN will be extracted from EMAIlADDRESS
</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
@@ -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.
Expand Down Expand Up @@ -123,6 +123,16 @@ public void x509WhenSubjectPrincipalRegexInLambdaThenUsesRegexToExtractPrincipal
// @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("[email protected]"));
// @formatter:on
}

@Test
public void x509WhenUserDetailsServiceNotConfiguredThenUsesBean() throws Exception {
this.spring.register(UserDetailsServiceBeanConfig.class).autowire();
Expand Down Expand Up @@ -277,6 +287,33 @@ UserDetailsService userDetailsService() {

}

@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("[email protected]")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}

}

@Configuration
@EnableWebSecurity
static class UserDetailsServiceBeanConfig {
Expand Down
17 changes: 17 additions & 0 deletions config/src/test/resources/max.cer
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICojCCAgugAwIBAgIBADANBgkqhkiG9w0BAQ0FADBuMQswCQYDVQQGEwJydTEP
MA0GA1UECAwGTW9zY293MQ8wDQYDVQQKDAZTcHJpbmcxFjAUBgNVBAMMDU1heCBC
YXRpc2NoZXYxJTAjBgkqhkiG9w0BCQEWFm1heGJhdGlzY2hldkBnbWFpbC5jb20w
HhcNMjUwNTE0MTcyODM5WhcNMjYwNTE0MTcyODM5WjBuMQswCQYDVQQGEwJydTEP
MA0GA1UECAwGTW9zY293MQ8wDQYDVQQKDAZTcHJpbmcxFjAUBgNVBAMMDU1heCBC
YXRpc2NoZXYxJTAjBgkqhkiG9w0BCQEWFm1heGJhdGlzY2hldkBnbWFpbC5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALVZ2K/iOINeHZ4XAV3QmNRgS+iB
Vw0fW07uzYkCoSZU1lOBQE0k8+fdM2+X9AsgwfRCE3tUZquPApEKynB5V9Seh+bR
vc9aj7PunMyN+zjRU6X7/BL3VqLfrJLSc15bQaSN1phJ6NT+BTXPTuiPbXldnJLC
wVo6PView83yZ335AgMBAAGjUDBOMB0GA1UdDgQWBBQhyQfxL2ZYotcS8AmMJtli
2IRAMTAfBgNVHSMEGDAWgBQhyQfxL2ZYotcS8AmMJtli2IRAMTAMBgNVHRMEBTAD
AQH/MA0GCSqGSIb3DQEBDQUAA4GBAIIIJxpsTPtUEnePAqqgVFWDKC2CExhtCBYL
MjLSC+7E9OlfuuX1joAsD4Yv86k4Ox836D0KQtINtg3y6D8O+HSylhVg1xtOiK7l
ElXVRepB8GcX3vf9F58v9s++cSDvXf8vJu/O7nI4fv9C5SfUtMY4JPh/3MTsyl8O
tgxTKjvO
-----END CERTIFICATE-----
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2223,6 +2223,9 @@ 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]]
== <filter-chain-map>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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 All @@ -16,6 +16,7 @@

package org.springframework.security.web.authentication.preauth.x509;

import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -43,7 +44,9 @@
* "[email protected], CN=..." giving a user name "[email protected]"
*
* @author Luke Taylor
* @deprecated Please use {@link SubjectX500PrincipalExtractor} instead
*/
@Deprecated
public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor, MessageSourceAware {

protected final Log logger = LogFactory.getLog(getClass());
Expand All @@ -59,6 +62,7 @@ public SubjectDnX509PrincipalExtractor() {
@Override
public Object extractPrincipal(X509Certificate clientCert) {
// String subjectDN = clientCert.getSubjectX500Principal().getName();
Principal principal = clientCert.getSubjectDN();
String subjectDN = clientCert.getSubjectDN().getName();
this.logger.debug(LogMessage.format("Subject DN is '%s'", subjectDN));
Matcher matcher = this.subjectDnPattern.matcher(subjectDN);
Expand Down
Loading