1
0
mirror of synced 2026-08-06 02:08:01 +00:00

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>
This commit is contained in:
Josh Cummings
2025-05-16 14:07:10 +03:00
parent cf06871200
commit e3ad551ab3
33 changed files with 1662 additions and 125 deletions
@@ -43,7 +43,9 @@ import org.springframework.util.Assert;
* "EMAILADDRESS=jimi@hendrix.org, CN=..." giving a user name "jimi@hendrix.org"
*
* @author Luke Taylor
* @deprecated Please use {@link SubjectX500PrincipalExtractor} instead
*/
@Deprecated
public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor, MessageSourceAware {
protected final Log logger = LogFactory.getLog(getClass());
@@ -0,0 +1,137 @@
/*
* Copyright 2004-present 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.
*/
package org.springframework.security.web.authentication.preauth.x509;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.security.auth.x500.X500Principal;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
/**
* Extracts the principal from the {@link X500Principal#getName(String)} returned by
* {@link X509Certificate#getSubjectX500Principal()} passed into
* {@link #extractPrincipal(X509Certificate)} depending on the value of
* {@link #setExtractPrincipalNameFromEmail(boolean)}.
*
* @author Max Batischev
* @author Rob Winch
* @since 6.5.11
*/
public final class SubjectX500PrincipalExtractor implements X509PrincipalExtractor, MessageSourceAware {
private final Log logger = LogFactory.getLog(getClass());
private static final String EMAIL_SUBJECT_DN_TYPE = "OID.1.2.840.113549.1.9.1";
private static final String CN_SUBJECT_DN_TYPE = "CN";
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private String subjectDnType = CN_SUBJECT_DN_TYPE;
private String x500PrincipalFormat = X500Principal.RFC2253;
@Override
public Object extractPrincipal(X509Certificate clientCert) {
Assert.notNull(clientCert, "clientCert cannot be null");
X500Principal principal = clientCert.getSubjectX500Principal();
String subjectDN = principal.getName(this.x500PrincipalFormat);
this.logger.debug(LogMessage.format("Subject DN is '%s'", subjectDN));
String principalName = getSubject(subjectDN);
this.logger.debug(LogMessage.format("Extracted Principal name is '%s'", principalName));
return principalName;
}
private List<Rdn> getDns(String subjectDn) {
try {
// read most-specific first, see gh-19254
List<Rdn> rdns = new ArrayList<>(new LdapName(subjectDn).getRdns());
Collections.reverse(rdns);
return rdns;
}
catch (InvalidNameException ex) {
throw new BadCredentialsException("Failed to parse client certificate", ex);
}
}
private String getSubject(String subjectDn) {
for (Rdn rdn : getDns(subjectDn)) {
String type = rdn.getType();
if (this.subjectDnType.equals(type)) {
return String.valueOf(rdn.getValue());
}
}
throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching",
new Object[] { subjectDn }, "No matching pattern was found in subject DN: {0}"));
}
@Override
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* Sets if the principal name should be extracted from the emailAddress or CN
* attribute (default).
*
* By default, the format {@link X500Principal#RFC2253} is passed to
* {@link X500Principal#getName(String)} and the principal is extracted from the CN
* attribute as defined in
* <a href="https://datatracker.ietf.org/doc/html/rfc2253#section-2.3">Converting
* AttributeTypeAndValue of RFC2253</a>.
*
* If {@link #setExtractPrincipalNameFromEmail(boolean)} is {@code true}, then the
* format {@link X500Principal#RFC2253} is passed to
* {@link X500Principal#getName(String)} and the principal is extracted from the
* <a href="https://oid-base.com/get/1.2.840.113549.1.9.1">OID.1.2.840.113549.1.9.1
* (emailAddress)</a> attribute as defined in
* <a href="https://datatracker.ietf.org/doc/html/rfc1779#section-2.3">Section 2.3 of
* RFC1779</a>.
* @param extractPrincipalNameFromEmail whether to extract the principal from the
* emailAddress (default false)
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2253">RFC2253</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfC1779">RFC1779</a>
*/
public void setExtractPrincipalNameFromEmail(boolean extractPrincipalNameFromEmail) {
if (extractPrincipalNameFromEmail) {
this.subjectDnType = EMAIL_SUBJECT_DN_TYPE;
this.x500PrincipalFormat = X500Principal.RFC1779;
}
else {
this.subjectDnType = CN_SUBJECT_DN_TYPE;
this.x500PrincipalFormat = X500Principal.RFC2253;
}
}
}
@@ -28,7 +28,7 @@ import org.springframework.security.web.authentication.preauth.AbstractPreAuthen
*/
public class X509AuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
private X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
private X509PrincipalExtractor principalExtractor = new SubjectX500PrincipalExtractor();
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
@@ -74,6 +74,13 @@ public class SubjectDnX509PrincipalExtractorTests {
assertThat(principal).isEqualTo("Duke");
}
// gh-19254
@Test
public void defaultCNPatternReturnsMostSpecificPrincipalWhenMultipleCns() throws Exception {
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithMultipleCns());
assertThat(principal).isEqualTo("alice");
}
@Test
public void setMessageSourceWhenNullThenThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null));
@@ -0,0 +1,90 @@
/*
* Copyright 2004-present 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.
*/
package org.springframework.security.web.authentication.preauth.x509;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link SubjectX500PrincipalExtractor}.
*
* @author Max Batischev
*/
public class SubjectX500PrincipalExtractorTests {
private final SubjectX500PrincipalExtractor extractor = new SubjectX500PrincipalExtractor();
@Test
void extractWhenCnPatternSetThenExtractsPrincipalName() throws Exception {
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificate());
assertThat(principal).isEqualTo("Luke Taylor");
}
@Test
void extractWhenEmailPatternSetThenExtractsPrincipalName() throws Exception {
this.extractor.setExtractPrincipalNameFromEmail(true);
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificate());
assertThat(principal).isEqualTo("luke@monkeymachine");
}
@Test
void extractWhenCnAtEndThenExtractsPrincipalName() throws Exception {
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithCnAtEnd());
assertThat(principal).isEqualTo("Duke");
}
@Test
void extractWhenDnEmbeddedInCnThenExtractsPrincipalName() throws Exception {
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedDn());
assertThat(principal).isEqualTo("luke");
}
// gh-19254
@Test
void extractWhenMultipleCnsThenExtractsMostSpecificCn() throws Exception {
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithMultipleCns());
assertThat(principal).isEqualTo("alice");
}
@Test
void extractWhenEmailDnEmbeddedInCnThenExtractsEmail() throws Exception {
this.extractor.setExtractPrincipalNameFromEmail(true);
Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedEmailDn());
assertThat(principal).isEqualTo("luke@monkeymachine");
}
@Test
void setMessageSourceWhenNullThenThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null));
}
@Test
void extractWhenCertificateIsNullThenFails() {
assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.extractPrincipal(null));
}
}
@@ -135,4 +135,88 @@ public final class X509TestUtils {
return (X509Certificate) cf.generateCertificate(in);
}
public static X509Certificate buildTestCertficateWithEmbeddedDn() throws Exception {
String cert = "-----BEGIN CERTIFICATE-----\n"
+ "MIIDDTCCAfWgAwIBAgIJANSyvk4gJhqPMA0GCSqGSIb3DQEBCwUAMEYxDTALBgNV\n"
+ "BAMMBGx1a2UxETAPBgNVBAsMCENOPWR1a2UsMRUwEwYDVQQKDAxFeGFtcGxlIENv\n"
+ "cnAxCzAJBgNVBAYTAlVTMB4XDTI2MDEwNDE5MjY0N1oXDTI3MDEwNTE5MjY0N1ow\n"
+ "RjENMAsGA1UEAwwEbHVrZTERMA8GA1UECwwIQ049ZHVrZSwxFTATBgNVBAoMDEV4\n"
+ "YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n"
+ "ggEKAoIBAQDU9fY74nEFbBKfIef7CK02J/BJb42sIF9kD8eHN5OvEwLQBeTh30it\n"
+ "E7LLalXyOXeUFkPe1N1ZhGdVak9udsIqULSvQaWqTbN+IrAGklZAxuXYTC1GbhMF\n"
+ "AkGWWM55J2SNqVGQaHzZUn6VPxWaDft6nZR0DxuvXMYM5kVG6VErdB3ygGUv8cjQ\n"
+ "QBKAYpsZeRldnauRPt2dImmGTagvSuJVyr8X/AioE2Rl0guii456AKw+QSvRiZ+g\n"
+ "w08Y8C9nDyzQmurqpdYYkp0X+4yqm1iVowMX+tSPvHnlqJdvVzaW2b0yRzrrT6ao\n"
+ "UCgw25slR1P1IcyzqPKWQIoQRnYIaX1bAgMBAAEwDQYJKoZIhvcNAQELBQADggEB\n"
+ "AIos+nr8DFM6bAt9AI/79O/12hcN7gVv4F3P4Vz6NRRkkvsb9WMN8fLLDEsEJ/BQ\n"
+ "eQkAVnhlmAe++vrqy8OTHoQ7F5C3K0zrr19NLNoyNFTkXkFgnm4ZhYinSbusuIb7\n"
+ "LPYoyCnEEiMdl0VMWWSWcOvZpipbvTtH3CiVxTqXLjFFNraEAyUN50kXjo/zuHpK\n"
+ "HzTS1BAu0li9GdV3Da2ELdDx90zaUym7dDIejY4YUlXYIJ5UUYS61fqtgOHGLLdb\n"
+ "UXGAr5gqEe7OrQ9D4ebg9w5ciTb7g1H2CmirjTf/rkii8AojmsGFKIfGVe3gY6EB\n" + "o9eF5FV9V9leo5yLo25ev08=\n"
+ "-----END CERTIFICATE-----";
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(in);
}
public static X509Certificate buildTestCertficateWithEmbeddedEmailDn() throws Exception {
String cert = "-----BEGIN CERTIFICATE-----\n"
+ "MIIDfDCCAmSgAwIBAgIIXHoOUFeZ29MwDQYJKoZIhvcNAQELBQAwfjEhMB8GCSqG\n"
+ "SIb3DQEJARYSbHVrZUBtb25rZXltYWNoaW5lMTUwMwYDVQQLDCxPSUQuMS4yLjg0\n"
+ "MC4xMTM1NDkuMS45LjE9ZHVrZUBnb3JpbGxhZ2FkZ2V0LDEVMBMGA1UECgwMRXhh\n"
+ "bXBsZSBDb3JwMQswCQYDVQQGEwJVUzAeFw0yNjAxMDQxOTMxMDhaFw0yNzAxMDUx\n"
+ "OTMxMDhaMH4xITAfBgkqhkiG9w0BCQEWEmx1a2VAbW9ua2V5bWFjaGluZTE1MDMG\n"
+ "A1UECwwsT0lELjEuMi44NDAuMTEzNTQ5LjEuOS4xPWR1a2VAZ29yaWxsYWdhZGdl\n"
+ "dCwxFTATBgNVBAoMDEV4YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqG\n"
+ "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBuIQWnj+uvvG+4ZIyFMs4dSbiBavubCmC\n"
+ "hudrHr93hP19QbPulbHRTVCUqEi8efvq+J9jmMdPd7tziuDX02PeG9uljp9+c5Ir\n"
+ "pw9/oMoTRkF7K4PK1JxLN4tcgxjxVA4QkS+MjKLPeHrYyGCjKspcHbi+zBiQ9Xqp\n"
+ "yHWq6N5XPd6mEj2gh0zamnsJCeUCOX4SJbcp3MFtcYzhguHAeVhy9Jv+EAMJejDn\n"
+ "YIZmMUdP6Ykf2zTzs/4L3bRZb0oS5WvfeRdJB6SKg8mNO/jdGX87krSio//cRdDy\n"
+ "TGQK+YCVDf8GyLLavYZW56AJbZxL3MWgHYilQjj4p+Kw/PWpaBVvAgMBAAEwDQYJ\n"
+ "KoZIhvcNAQELBQADggEBAKVTMIo8JO0H0HRrpsEDP17E2pnfMJV4g70BwClUMMek\n"
+ "wNIWZn+6XPR8oObzzjnVWXjrovMkmmyFk0vWIpF68MPyiQ++5fwdzOZiQtUP177n\n"
+ "9ulAtLoIJld3olGeL9VsCZGp3J2PqiDe613zd+bkSUG1lQYC2awozWqJEdvwJJtf\n"
+ "j9nlhyMsARKEEu3tFGJsCHST3XhbhFKOraf/GZ21xW650R7ap0ZNaEiB16M2a5Oe\n"
+ "WXasgUukIo82Z8+yK4IITeCcr0aA1fJxwhU8J6qfYWloaoirSYj487HRnPPv3X/b\n"
+ "RxZynIjtGKygT6T1dRaWennmoitqfprJnEO2tlhLwP0=\n" + "-----END CERTIFICATE-----";
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(in);
}
/**
* Builds an X.509 certificate whose subject contains more than one CN. The subject DN
* is:
*
* <pre>
* CN=alice, CN=bob, O=Example Corp, C=US
* </pre>
*
* where {@code CN=alice} is the most specific (left-most) RDN.
*/
public static X509Certificate buildTestCertificateWithMultipleCns() throws Exception {
String cert = "-----BEGIN CERTIFICATE-----\n"
+ "MIIDJzCCAg+gAwIBAgIIclOz1VulWC8wDQYJKoZIhvcNAQEMBQAwQjELMAkGA1UE\n"
+ "BhMCVVMxFTATBgNVBAoTDEV4YW1wbGUgQ29ycDEMMAoGA1UEAxMDYm9iMQ4wDAYD\n"
+ "VQQDEwVhbGljZTAeFw0yNjA2MDExOTQzMTVaFw0zNjA1MjkxOTQzMTVaMEIxCzAJ\n"
+ "BgNVBAYTAlVTMRUwEwYDVQQKEwxFeGFtcGxlIENvcnAxDDAKBgNVBAMTA2JvYjEO\n"
+ "MAwGA1UEAxMFYWxpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCX\n"
+ "q8hZrTRHJEN7+D6yK65OKeCTVU+WccI6awz6g4T6O3aoC+1IiUljsEYn+xuDfx5L\n"
+ "L/O1kejjmbYt+vzRmiILoJ9xKfW3ERcB4+gEar959Dkj6wmpsgOKRjmOvcOFOkEe\n"
+ "gU1F7t04JHOou3DaAkHMNMQV+3jsWSh9Rry7XZBDkcT8XbbagoCgSIIef03Qtw31\n"
+ "zOBwqmJmd6CQhFJva5cl8cCE2xZinOIiz/2j7VprZTjhkud2M4bRnK3T0WExyOCg\n"
+ "jvjSf5ZoOKwC2Z9Q/Oyf8WKpren1+GfZhAKKmn7QZ2foHhdPPRtNjRGE6SqQyW2u\n"
+ "8+1tOXl+aRCF19rR7gIpAgMBAAGjITAfMB0GA1UdDgQWBBRfLtnK5WKU0q3zxIrr\n"
+ "y3GD+lYplzANBgkqhkiG9w0BAQwFAAOCAQEAe+/FHqErVPsF/sHrVHny8mIsn3ux\n"
+ "qE9P24KNF0oIfmBrAqqge6hoVQ8PS+JialyqFf//osuDjiuYaBEKBw7GCoA6I8mr\n"
+ "FA7wyFaGosfq7An5vxkJl7lap2u5oSVv3dCy13Bs0ziYmNlTkfHDLy9yh7jpH1wg\n"
+ "TvylH9O4Vc7y9rzzpIjMCuQJ/wJ4MuJ2mSarYZsx3UQHIRfpKtR/9jbFMX1Rbv/A\n"
+ "N0XD+NrtFjiikp71y3aCO1EHnGG7qPKCWh3PzaNoWFpyZKDBvud8ymW7RMiLPgv9\n"
+ "eTa82KGgnCwtKCNzGkszIn7fza/6xnCuzvh4y9tYE5BWP2mcMRtRmDD07w==\n" + "-----END CERTIFICATE-----";
ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes());
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(in);
}
}