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:
+2
@@ -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());
|
||||
|
||||
+137
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+7
@@ -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));
|
||||
|
||||
+90
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+84
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user