diff --git a/core/src/main/java/org/springframework/security/ui/preauth/x509/SubjectDnX509PrincipalExtractor.java b/core/src/main/java/org/springframework/security/ui/preauth/x509/SubjectDnX509PrincipalExtractor.java new file mode 100644 index 0000000000..0b2c82ef00 --- /dev/null +++ b/core/src/main/java/org/springframework/security/ui/preauth/x509/SubjectDnX509PrincipalExtractor.java @@ -0,0 +1,84 @@ +package org.springframework.security.ui.preauth.x509; + +import org.springframework.security.BadCredentialsException; +import org.springframework.security.SpringSecurityMessageSource; +import org.springframework.util.Assert; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.context.MessageSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.security.cert.X509Certificate; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +/** + * Obtains the principal from a certificate using a regular expression match against the Subject (as returned by a call + * to {@link X509Certificate#getSubjectDN()}). + *
+ * The regular expression should contain a single group; for example the default expression "CN=(.?)," matches the + * common name field. So "CN=Jimi Hendrix, OU=..." will give a user name of "Jimi Hendrix". + *
+ * The matches are case insensitive. So "emailAddress=(.?)," will match "EMAILADDRESS=jimi@hendrix.org, CN=..." giving a + * user name "jimi@hendrix.org" + * + * @author Luke Taylor + * @version $Id$ + */ +public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor { + //~ Instance fields ================================================================================================ + protected final Log logger = LogFactory.getLog(getClass()); + protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + + private Pattern subjectDnPattern; + + public SubjectDnX509PrincipalExtractor() { + setSubjectDnRegex("CN=(.*?),"); + } + + public Object extractPrincipal(X509Certificate clientCert) { + //String subjectDN = clientCert.getSubjectX500Principal().getName(); + String subjectDN = clientCert.getSubjectDN().getName(); + + logger.debug("Subject DN is '" + subjectDN + "'"); + + Matcher matcher = subjectDnPattern.matcher(subjectDN); + + if (!matcher.find()) { + throw new BadCredentialsException(messages.getMessage("DaoX509AuthoritiesPopulator.noMatching", + new Object[] {subjectDN}, "No matching pattern was found in subject DN: {0}")); + } + + if (matcher.groupCount() != 1) { + throw new IllegalArgumentException("Regular expression must contain a single group "); + } + + String username = matcher.group(1); + + logger.debug("Extracted Principal name is '" + username + "'"); + + return username; + } + + /** + * Sets the regular expression which will by used to extract the user name from the certificate's Subject + * DN. + *
+ * It should contain a single group; for example the default expression "CN=(.?)," matches the common + * name field. So "CN=Jimi Hendrix, OU=..." will give a user name of "Jimi Hendrix". + *
+ * The matches are case insensitive. So "emailAddress=(.?)," will match "EMAILADDRESS=jimi@hendrix.org, + * CN=..." giving a user name "jimi@hendrix.org" + * + * @param subjectDnRegex the regular expression to find in the subject + */ + public void setSubjectDnRegex(String subjectDnRegex) { + Assert.hasText(subjectDnRegex, "Regular expression may not be null or empty"); + subjectDnPattern = Pattern.compile(subjectDnRegex, Pattern.CASE_INSENSITIVE); + } + + public void setMessageSource(MessageSource messageSource) { + this.messages = new MessageSourceAccessor(messageSource); + } +} diff --git a/core/src/main/java/org/springframework/security/ui/preauth/x509/X509PreAuthenticatedProcessingFilter.java b/core/src/main/java/org/springframework/security/ui/preauth/x509/X509PreAuthenticatedProcessingFilter.java new file mode 100644 index 0000000000..a047ae16d7 --- /dev/null +++ b/core/src/main/java/org/springframework/security/ui/preauth/x509/X509PreAuthenticatedProcessingFilter.java @@ -0,0 +1,51 @@ +package org.springframework.security.ui.preauth.x509; + +import org.springframework.security.ui.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.ui.FilterChainOrder; + +import javax.servlet.http.HttpServletRequest; +import java.security.cert.X509Certificate; + +/** + * @author Luke Taylor + * @version $Id$ + */ +public class X509PreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter { + private X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); + + protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { + X509Certificate cert = extractClientCertificate(request); + + if (cert == null) { + return null; + } + + return principalExtractor.extractPrincipal(cert); + } + + protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { + return extractClientCertificate(request); + } + + private X509Certificate extractClientCertificate(HttpServletRequest request) { + X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); + + if (certs != null && certs.length > 0) { + return certs[0]; + } + + if (logger.isDebugEnabled()) { + logger.debug("No client certificate found in request."); + } + + return null; + } + + public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { + this.principalExtractor = principalExtractor; + } + + public int getOrder() { + return FilterChainOrder.X509_FILTER; + } +} diff --git a/core/src/main/java/org/springframework/security/ui/preauth/x509/X509PrincipalExtractor.java b/core/src/main/java/org/springframework/security/ui/preauth/x509/X509PrincipalExtractor.java new file mode 100644 index 0000000000..ace868c01f --- /dev/null +++ b/core/src/main/java/org/springframework/security/ui/preauth/x509/X509PrincipalExtractor.java @@ -0,0 +1,17 @@ +package org.springframework.security.ui.preauth.x509; + +import java.security.cert.X509Certificate; + +/** + * Obtains the principal from an X509Certificate for use within the framework. + * + * @author Luke Taylor + * @version $Id$ + */ +public interface X509PrincipalExtractor { + + /** + * Returns the principal (usually a String) for the given certificate. + */ + Object extractPrincipal(X509Certificate cert); +} diff --git a/core/src/test/java/org/springframework/security/ui/preauth/x509/SubjectDnX509PrincipalExtractorTests.java b/core/src/test/java/org/springframework/security/ui/preauth/x509/SubjectDnX509PrincipalExtractorTests.java new file mode 100644 index 0000000000..d3433138a6 --- /dev/null +++ b/core/src/test/java/org/springframework/security/ui/preauth/x509/SubjectDnX509PrincipalExtractorTests.java @@ -0,0 +1,48 @@ +package org.springframework.security.ui.preauth.x509; + +import org.springframework.security.providers.x509.X509TestUtils; +import org.springframework.security.SpringSecurityMessageSource; +import org.springframework.security.BadCredentialsException; + +import org.junit.Test; +import org.junit.Before; + +import static junit.framework.Assert.*; + +/** + * @author Luke Taylor + * @version $Id$ + */ +public class SubjectDnX509PrincipalExtractorTests { + SubjectDnX509PrincipalExtractor extractor; + + @Before + public void setUp() { + extractor = new SubjectDnX509PrincipalExtractor(); + extractor.setMessageSource(new SpringSecurityMessageSource()); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidRegexFails() throws Exception { + extractor.setSubjectDnRegex("CN=(.*?,"); // missing closing bracket on group + } + + @Test + public void defaultCNPatternReturnsExcpectedPrincipal() throws Exception { + Object principal = extractor.extractPrincipal(X509TestUtils.buildTestCertificate()); + assertEquals("Luke Taylor", principal); + } + + @Test + public void matchOnEmailReturnsExpectedPrincipal() throws Exception { + extractor.setSubjectDnRegex("emailAddress=(.*?),"); + Object principal = extractor.extractPrincipal(X509TestUtils.buildTestCertificate()); + assertEquals("luke@monkeymachine", principal); + } + + @Test(expected = BadCredentialsException.class) + public void matchOnShoeSizeThrowsBadCredentials() throws Exception { + extractor.setSubjectDnRegex("shoeSize=(.*?),"); + extractor.extractPrincipal(X509TestUtils.buildTestCertificate()); + } +}