From ae91b5868572919ad2dce65e89118658765b711d Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Wed, 9 Mar 2005 02:14:30 +0000 Subject: [PATCH] First stab at X509 authentication provider --- .../x509/X509AuthenticationProvider.java | 66 ++++++++++++++ .../x509/X509AuthenticationToken.java | 57 ++++++++++++ .../ui/x509/X509ProcessingFilter.java | 76 ++++++++++++++++ .../x509/X509AuthenticationProviderTests.java | 63 ++++++++++++++ .../x509/X509AuthenticationTokenTests.java | 32 +++++++ .../providers/x509/X509TestUtils.java | 87 +++++++++++++++++++ 6 files changed, 381 insertions(+) create mode 100644 core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationProvider.java create mode 100644 core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationToken.java create mode 100644 core/src/main/java/org/acegisecurity/ui/x509/X509ProcessingFilter.java create mode 100644 core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationProviderTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationTokenTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/x509/X509TestUtils.java diff --git a/core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationProvider.java new file mode 100644 index 0000000000..bc0574227f --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationProvider.java @@ -0,0 +1,66 @@ +package net.sf.acegisecurity.providers.x509; + +import net.sf.acegisecurity.providers.AuthenticationProvider; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.UserDetails; +import org.springframework.beans.factory.InitializingBean; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.security.cert.X509Certificate; + +/** + * @author Luke Taylor + */ +public class X509AuthenticationProvider implements AuthenticationProvider, + InitializingBean { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(X509AuthenticationProvider.class); + + //~ Instance fields ======================================================== + private X509AuthoritiesPopulator x509AuthoritiesPopulator; + + //~ Methods ================================================================ + + public void setX509AuthoritiesPopulator(X509AuthoritiesPopulator x509AuthoritiesPopulator) { + this.x509AuthoritiesPopulator = x509AuthoritiesPopulator; + } + + public void afterPropertiesSet() throws Exception { + if(x509AuthoritiesPopulator == null) { + throw new IllegalArgumentException("An X509AuthoritiesPopulator must be set"); + } + } + + /** + * + * @param authentication + * @return + * @throws AuthenticationException if the {@link X509AuthoritiesPopulator} rejects the certficate + */ + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!supports(authentication.getClass())) { + return null; + } + + if(logger.isDebugEnabled()) + logger.debug("X509 authentication request: " + authentication); + + X509Certificate clientCertificate = (X509Certificate)authentication.getCredentials(); + + // TODO: Cache + + + // Lookup user details for the given certificate + UserDetails userDetails = x509AuthoritiesPopulator.getUserDetails(clientCertificate); + + return new X509AuthenticationToken(userDetails, clientCertificate, userDetails.getAuthorities()); + } + + public boolean supports(Class authentication) { + return X509AuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationToken.java b/core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationToken.java new file mode 100644 index 0000000000..795e021bc2 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/x509/X509AuthenticationToken.java @@ -0,0 +1,57 @@ +package net.sf.acegisecurity.providers.x509; + +import net.sf.acegisecurity.providers.AbstractAuthenticationToken; +import net.sf.acegisecurity.GrantedAuthority; + +import javax.security.cert.Certificate; +import java.security.cert.X509Certificate; + +/** + * Authentication implementation for X.509 client-certificate authentication. + * + * @author Luke Taylor + * @version $Id$ + */ +public class X509AuthenticationToken extends AbstractAuthenticationToken { + //~ Instance fields ======================================================== + + private X509Certificate credentials; + private Object principal; + private GrantedAuthority[] authorities; + private boolean authenticated = false; + + //~ Constructors =========================================================== + + /** Used for an authentication request */ + public X509AuthenticationToken(X509Certificate credentials) { + this.credentials = credentials; + } + + public X509AuthenticationToken(Object principal, X509Certificate credentials, GrantedAuthority[] authorities) { + this.credentials = credentials; + this.principal = principal; + this.authorities = authorities; + } + + //~ Methods ================================================================ + + public void setAuthenticated(boolean isAuthenticated) { + this.authenticated = isAuthenticated; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public GrantedAuthority[] getAuthorities() { + return authorities; + } + + public Object getCredentials() { + return credentials; + } + + public Object getPrincipal() { + return principal; + } +} diff --git a/core/src/main/java/org/acegisecurity/ui/x509/X509ProcessingFilter.java b/core/src/main/java/org/acegisecurity/ui/x509/X509ProcessingFilter.java new file mode 100644 index 0000000000..a2155af22c --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/x509/X509ProcessingFilter.java @@ -0,0 +1,76 @@ +package net.sf.acegisecurity.ui.x509; + +import net.sf.acegisecurity.ui.AbstractProcessingFilter; +import net.sf.acegisecurity.ui.WebAuthenticationDetails; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.context.ContextHolder; +import net.sf.acegisecurity.context.security.SecureContext; +import net.sf.acegisecurity.providers.x509.X509AuthenticationToken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.security.cert.X509Certificate; + +/** + * Processes the X.509 certificate submitted by a client - typically + * when HTTPS is used with client-authentiction enabled. + *

+ * An {@link X509AuthenticationToken} is created with the certificate + * as the credentials. + *

+ *

+ * The configured authentication manager is expected to supply a + * provider which can handle this token (usually an instance of + * {@link net.sf.acegisecurity.providers.x509.X509AuthenticationProvider}). + *

+ * + *

+ * Do not use this class directly. Instead configure + * web.xml to use the {@link + * net.sf.acegisecurity.util.FilterToBeanProxy}. + *

+ * + * @author Luke Taylor + */ +public class X509ProcessingFilter extends AbstractProcessingFilter { + + public String getDefaultFilterProcessesUrl() { + return "/*"; + } + + /** + * X.509 authentication doesn't have a specific login URL, so the default implementation + * using endsWith isn't adequate. + * + */ + protected boolean requiresAuthentication(HttpServletRequest request, + HttpServletResponse response) { + return true; // for the time being. Should probably do a pattern match on the URL + } + + /** + * + * @param request the request containing the client certificate + * @return + * @throws AuthenticationException if the authentication manager rejects the certificate for some reason. + */ + public Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException { + X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); + + X509Certificate clientCertificate = null; + + if(certs != null && certs.length > 0) { + clientCertificate = certs[0]; + } else { + logger.warn("No client certificate found in Request."); + } + // TODO: warning is probably superfluous, as it may get called when a non-protected URL is used and no certificate is present. + + X509AuthenticationToken authRequest = new X509AuthenticationToken(clientCertificate); + + // authRequest.setDetails(new WebAuthenticationDetails(request)); + + return this.getAuthenticationManager().authenticate(authRequest); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationProviderTests.java new file mode 100644 index 0000000000..4c84394e30 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationProviderTests.java @@ -0,0 +1,63 @@ +package net.sf.acegisecurity.providers.x509; + +import junit.framework.TestCase; +import net.sf.acegisecurity.*; +import net.sf.acegisecurity.providers.dao.User; + +import java.security.cert.X509Certificate; + +/** + * @author Luke Taylor + */ +public class X509AuthenticationProviderTests extends TestCase { + //~ Constructors =========================================================== + + public X509AuthenticationProviderTests() { + super(); + } + + public X509AuthenticationProviderTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public void testAuthenticationInvalidCertificate() throws Exception { + X509AuthenticationProvider provider = new X509AuthenticationProvider(); + provider.setX509AuthoritiesPopulator(new MockAuthoritiesPopulator(true)); + try { + provider.authenticate(X509TestUtils.createToken()); + fail("Should have thrown BadCredentialsException"); + } catch(BadCredentialsException e) { + //ignore + } + } + + + //~ Inner Classes ========================================================== + + public static class MockAuthoritiesPopulator implements X509AuthoritiesPopulator { + private boolean rejectCertificate; + + public MockAuthoritiesPopulator(boolean rejectCertificate) { + this.rejectCertificate = rejectCertificate; + } + + public UserDetails getUserDetails(X509Certificate userCertificate) throws AuthenticationException { + if(rejectCertificate) { + throw new BadCredentialsException("Invalid Certificate"); + } + + return new User ("user", "password", true, true, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl( + "ROLE_B")}); + } + } + + + +} diff --git a/core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationTokenTests.java b/core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationTokenTests.java new file mode 100644 index 0000000000..de7e17d821 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/x509/X509AuthenticationTokenTests.java @@ -0,0 +1,32 @@ +package net.sf.acegisecurity.providers.x509; + +import junit.framework.TestCase; + +import java.security.cert.X509Certificate; +import java.security.cert.CertificateFactory; +import java.io.ByteArrayInputStream; + +/** + * @author Luke Taylor + */ +public class X509AuthenticationTokenTests extends TestCase { + + public X509AuthenticationTokenTests() { + } + + public X509AuthenticationTokenTests(String s) { + super(s); + } + + public void setUp() throws Exception { + super.setUp(); + } + + public void testAuthenticated() throws Exception { + X509AuthenticationToken token = X509TestUtils.createToken(); + assertTrue(!token.isAuthenticated()); + token.setAuthenticated(true); + assertTrue(token.isAuthenticated()); + } +} + diff --git a/core/src/test/java/org/acegisecurity/providers/x509/X509TestUtils.java b/core/src/test/java/org/acegisecurity/providers/x509/X509TestUtils.java new file mode 100644 index 0000000000..b83874a790 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/x509/X509TestUtils.java @@ -0,0 +1,87 @@ +package net.sf.acegisecurity.providers.x509; + +import java.security.cert.X509Certificate; +import java.security.cert.CertificateFactory; +import java.io.ByteArrayInputStream; + +/** + * @author Luke Taylor + */ +public class X509TestUtils { + + public static X509AuthenticationToken createToken() throws Exception { + return new X509AuthenticationToken(buildTestCertificate()); + } + /** + * Builds an X.509 certificate. In human-readable form it is: + *
+      * Certificate:
+      *   Data:
+      *      Version: 3 (0x2)
+      *      Serial Number: 1 (0x1)
+      *      Signature Algorithm: sha1WithRSAEncryption
+      *      Issuer: CN=Monkey Machine CA, C=UK, ST=Scotland, L=Glasgow,
+      *          O=monkeymachine.co.uk/emailAddress=ca@monkeymachine.co.uk
+      *      Validity
+      *          Not Before: Mar  6 23:28:22 2005 GMT
+      *          Not After : Mar  6 23:28:22 2006 GMT
+      *      Subject: C=UK, ST=Scotland, L=Glasgow, O=Monkey Machine Ltd,
+      *          OU=Open Source Development Lab., CN=Luke Taylor/emailAddress=luke@monkeymachine
+      *      Subject Public Key Info:
+      *          Public Key Algorithm: rsaEncryption
+      *          RSA Public Key: (512 bit)
+      *              [omitted]
+      *      X509v3 extensions:
+      *          X509v3 Basic Constraints:
+      *          CA:FALSE
+      *          Netscape Cert Type:
+      *          SSL Client
+      *          X509v3 Key Usage:
+      *          Digital Signature, Non Repudiation, Key Encipherment
+      *          X509v3 Subject Key Identifier:
+      *          6E:E6:5B:57:33:CF:0E:2F:15:C2:F4:DF:EC:14:BE:FB:CF:54:56:3C
+      *          X509v3 Authority Key Identifier:
+      *          keyid:AB:78:EC:AF:10:1B:8A:9B:1F:C7:B1:25:8F:16:28:F2:17:9A:AD:36
+      *          DirName:/CN=Monkey Machine CA/C=UK/ST=Scotland/L=Glasgow/O=monkeymachine.co.uk/emailAddress=ca@monkeymachine.co.uk
+      *          serial:00
+      *          Netscape CA Revocation Url:
+      *          https://monkeymachine.co.uk/ca-crl.pem
+      *   Signature Algorithm: sha1WithRSAEncryption
+      *             [signature omitted]
+      * 
+ */ + public static X509Certificate buildTestCertificate() throws Exception + { + String cert = "-----BEGIN CERTIFICATE-----\n" + + "MIIEQTCCAymgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBkzEaMBgGA1UEAxMRTW9u\n" + + "a2V5IE1hY2hpbmUgQ0ExCzAJBgNVBAYTAlVLMREwDwYDVQQIEwhTY290bGFuZDEQ\n" + + "MA4GA1UEBxMHR2xhc2dvdzEcMBoGA1UEChMTbW9ua2V5bWFjaGluZS5jby51azEl\n" + + "MCMGCSqGSIb3DQEJARYWY2FAbW9ua2V5bWFjaGluZS5jby51azAeFw0wNTAzMDYy\n" + + "MzI4MjJaFw0wNjAzMDYyMzI4MjJaMIGvMQswCQYDVQQGEwJVSzERMA8GA1UECBMI\n" + + "U2NvdGxhbmQxEDAOBgNVBAcTB0dsYXNnb3cxGzAZBgNVBAoTEk1vbmtleSBNYWNo\n" + + "aW5lIEx0ZDElMCMGA1UECxMcT3BlbiBTb3VyY2UgRGV2ZWxvcG1lbnQgTGFiLjEU\n" + + "MBIGA1UEAxMLTHVrZSBUYXlsb3IxITAfBgkqhkiG9w0BCQEWEmx1a2VAbW9ua2V5\n" + + "bWFjaGluZTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDItxZr07mm65ttYH7RMaVo\n" + + "VeMCq4ptfn+GFFEk4+54OkDuh1CHlk87gEc1jx3ZpQPJRTJx31z3YkiAcP+RDzxr\n" + + "AgMBAAGjggFIMIIBRDAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIHgDALBgNV\n" + + "HQ8EBAMCBeAwHQYDVR0OBBYEFG7mW1czzw4vFcL03+wUvvvPVFY8MIHABgNVHSME\n" + + "gbgwgbWAFKt47K8QG4qbH8exJY8WKPIXmq02oYGZpIGWMIGTMRowGAYDVQQDExFN\n" + + "b25rZXkgTWFjaGluZSBDQTELMAkGA1UEBhMCVUsxETAPBgNVBAgTCFNjb3RsYW5k\n" + + "MRAwDgYDVQQHEwdHbGFzZ293MRwwGgYDVQQKExNtb25rZXltYWNoaW5lLmNvLnVr\n" + + "MSUwIwYJKoZIhvcNAQkBFhZjYUBtb25rZXltYWNoaW5lLmNvLnVrggEAMDUGCWCG\n" + + "SAGG+EIBBAQoFiZodHRwczovL21vbmtleW1hY2hpbmUuY28udWsvY2EtY3JsLnBl\n" + + "bTANBgkqhkiG9w0BAQUFAAOCAQEAZ961bEgm2rOq6QajRLeoljwXDnt0S9BGEWL4\n" + + "PMU2FXDog9aaPwfmZ5fwKaSebwH4HckTp11xwe/D9uBZJQ74Uf80UL9z2eo0GaSR\n" + + "nRB3QPZfRvop0I4oPvwViKt3puLsi9XSSJ1w9yswnIf89iONT7ZyssPg48Bojo8q\n" + + "lcKwXuDRBWciODK/xWhvQbaegGJ1BtXcEHtvNjrUJLwSMDSr+U5oUYdMohG0h1iJ\n" + + "R+JQc49I33o2cTc77wfEWLtVdXAyYY4GSJR6VfgvV40x85ItaNS3HHfT/aXU1x4m\n" + + "W9YQkWlA6t0blGlC+ghTOY1JbgWnEfXMmVgg9a9cWaYQ+NQwqA==\n" + + "-----END CERTIFICATE-----"; + + ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate)cf.generateCertificate(in); + + } + +}