blob: e78d7688f8c024804244c13040227f2e6ea93130 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.apache.ace.authentication.processor.clientcert;
import static org.apache.ace.authentication.processor.clientcert.ClientCertAuthenticationProcessor.ATTRIBUTE_CIPHER_SUITE;
import static org.apache.ace.authentication.processor.clientcert.ClientCertAuthenticationProcessor.ATTRIBUTE_X509_CERTIFICATE;
import static org.apache.ace.authentication.processor.clientcert.ClientCertAuthenticationProcessor.PROPERTY_USERNAME_LOOKUPKEY;
import static org.apache.ace.authentication.processor.clientcert.ClientCertAuthenticationProcessor.PROPERTY_USERNAME_MATCH_POLICY;
import static org.apache.ace.authentication.processor.clientcert.ClientCertAuthenticationProcessor.PROPERTY_VERIFY_CERT_VALIDITY;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Date;
import java.util.Dictionary;
import java.util.Hashtable;
import javax.security.auth.x500.X500Principal;
import javax.servlet.http.HttpServletRequest;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.log.LogService;
import org.osgi.service.useradmin.User;
import org.osgi.service.useradmin.UserAdmin;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
* Test cases for {@link ClientCertAuthenticationProcessor}.
*/
public class ClientCertAuthenticationProcessorTest {
private static MemoryKeyStore m_keystore;
private LogService m_log;
private UserAdmin m_userAdmin;
private HttpServletRequest m_servletRequest;
/**
* @return the day after tomorrow, never <code>null</code>.
*/
private static Date dayAfterTomorrow() {
Calendar cal = getToday();
cal.add(Calendar.DAY_OF_MONTH, +2);
return cal.getTime();
}
/**
* @return the day before yesterday, never <code>null</code>.
*/
private static Date dayBeforeYesterday() {
Calendar cal = getToday();
cal.add(Calendar.DAY_OF_MONTH, -2);
return cal.getTime();
}
/**
* @return today as date, without time component, never <code>null</code>.
*/
private static Calendar getToday() {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 12);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return cal;
}
/**
* @return the date of tomorrow, never <code>null</code>.
*/
private static Date tomorrow() {
Calendar cal = getToday();
cal.add(Calendar.DAY_OF_MONTH, +1);
return cal.getTime();
}
/**
* @return the date of yesterday, never <code>null</code>.
*/
private static Date yesterday() {
Calendar cal = getToday();
cal.add(Calendar.DAY_OF_MONTH, -1);
return cal.getTime();
}
/**
* Creates an in-memory keystore for this test case.
*/
@BeforeClass(alwaysRun = true)
public static void init() {
m_keystore = new MemoryKeyStore("cn=testCA", dayBeforeYesterday(), dayAfterTomorrow());
}
/**
* Set up for each individual test.
*/
@BeforeMethod(alwaysRun = true)
public void setUp() {
m_log = mock(LogService.class);
m_userAdmin = mock(UserAdmin.class);
m_servletRequest = mock(HttpServletRequest.class);
when(m_servletRequest.getAttribute(ATTRIBUTE_CIPHER_SUITE)).thenReturn("bogus-cipher-suite");
}
/**
* Tests that a null certificate chain will yield null.
*/
@Test()
public void testAuthenticateNoCertificateChainYieldsNull() {
User result = createAuthorizationProcessor().authenticate(m_userAdmin, m_servletRequest);
assertNull(result, "Did not expect a valid user to be returned!");
}
/**
* Tests that an empty certificate chain will yield null.
*/
@Test()
public void testAuthenticateEmptyCertificateChainYieldsNull() {
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(new X509Certificate[0]);
User result = createAuthorizationProcessor().authenticate(m_userAdmin, m_servletRequest);
assertNull(result, "Did not expect a valid user to be returned!");
}
/**
* Tests that authenticating a known user with an invalid (expired) certificate will yield null.
*/
@Test()
public void testAuthenticateKnownUserWithExpiredCertificateYieldsNull() {
X509Certificate[] certificateChain = createExpiredCertificateChain("bob");
PublicKey publickey = certificateChain[0].getPublicKey();
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(certificateChain);
User user = mock(User.class);
when(user.getName()).thenReturn("bob");
when(user.hasCredential(eq("publickey"), eq(publickey.getEncoded()))).thenReturn(Boolean.TRUE);
when(m_userAdmin.getUser(eq("username"), eq("bob"))).thenReturn(user);
User result = createAuthorizationProcessor().authenticate(m_userAdmin, m_servletRequest);
assertNull(result, "Did not expect a valid user to be returned!");
}
/**
* Tests that authenticating a known user with an invalid (not valid) certificate will yield null.
*/
@Test()
public void testAuthenticateKnownUserWithNotValidCertificateYieldsNull() {
X509Certificate[] certificateChain = createExpiredCertificateChain("bob");
PublicKey publickey = certificateChain[0].getPublicKey();
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(
createNotValidCertificateChain("bob"));
User user = mock(User.class);
when(user.getName()).thenReturn("bob");
when(user.hasCredential(eq("publickey"), eq(publickey.getEncoded()))).thenReturn(Boolean.TRUE);
when(m_userAdmin.getUser(eq("username"), eq("bob"))).thenReturn(user);
User result = createAuthorizationProcessor().authenticate(m_userAdmin, m_servletRequest);
assertNull(result, "Did not expect a valid user to be returned!");
}
/**
* Tests that authenticating a known user with a valid certificate will not yield null.
*/
@Test()
public void testAuthenticateKnownUserYieldsValidResult() {
X509Certificate[] certChain = createValidCertificateChain("bob");
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(certChain);
User user = mock(User.class);
when(user.getName()).thenReturn("bob");
when(m_userAdmin.getUser(eq("username"), eq("bob"))).thenReturn(user);
User result = createAuthorizationProcessor().authenticate(m_userAdmin, m_servletRequest);
assertNotNull(result, "Expected a valid user to be returned!");
assertEquals(user.getName(), "bob", "Expected bob to be returned as user!");
}
/**
* Tests that authenticating a known user with a valid certificate chain will not yield null.
*/
@Test()
public void testAuthenticateKnownUserWithValidCertificateChainYieldsValidResult() throws ConfigurationException {
ClientCertAuthenticationProcessor processor = createAuthorizationProcessor();
final String lookupKey = "anyKey";
final String matchPolicy = "dn";
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_LOOKUPKEY, lookupKey);
props.put(PROPERTY_USERNAME_MATCH_POLICY, matchPolicy);
props.put(PROPERTY_VERIFY_CERT_VALIDITY, "true");
processor.updated(props);
X509Certificate[] certChain = createValidCertificateChainWithDN("cn=Alice,dc=acme,dc=corp", "cn=Fido,ou=dev,dc=acme,dc=corp", "cn=Bob,ou=dev,dc=acme,dc=corp");
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(certChain);
User user = mock(User.class);
when(user.getName()).thenReturn("bob");
when(m_userAdmin.getUser(eq(lookupKey), eq("DC=corp,DC=acme,OU=dev,CN=Bob"))).thenReturn(user);
User result = processor.authenticate(m_userAdmin, m_servletRequest);
assertNotNull(result, "Expected a valid user to be returned!");
assertEquals(user.getName(), "bob", "Expected bob to be returned as user!");
}
/**
* Tests that a missing cipher suite header will the authenticate method to yield null.
*/
@Test()
public void testAuthenticateMissingCipherSuiteHeaderYieldsNull() {
when(m_servletRequest.getAttribute(ATTRIBUTE_CIPHER_SUITE)).thenReturn(null);
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(createValidCertificateChain("bob"));
User result = createAuthorizationProcessor().authenticate(m_userAdmin, m_servletRequest);
assertNull(result, "Did not expect a valid user to be returned!");
}
/**
* Tests that a class cast exception is thrown for invalid context when calling authenticate.
*/
@Test(expectedExceptions = ClassCastException.class)
public void testAuthenticateThrowsClassCastForInvalidContext() {
createAuthorizationProcessor().authenticate(m_userAdmin, new Object());
}
/**
* Tests that an unknown user will yield null.
*/
@Test()
public void testAuthenticateUnknownUserYieldsNull() {
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(createValidCertificateChain("bob"));
User result = createAuthorizationProcessor().authenticate(m_userAdmin, m_servletRequest);
assertNull(result, "Did not expect a valid user to be returned!");
}
/**
* Tests that canHandle yields false for any object other than {@link HttpServletRequest}.
*/
@Test()
public void testCanHandleDoesAcceptServletRequest() {
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(createValidCertificateChain("alice"));
assertTrue(createAuthorizationProcessor().canHandle(m_servletRequest));
}
/**
* Tests that canHandle throws an {@link IllegalArgumentException} for an empty context.
*/
@Test(expectedExceptions = IllegalArgumentException.class)
public void testCanHandleDoesNotAcceptEmptyArray() {
createAuthorizationProcessor().canHandle(new Object[0]);
}
/**
* Tests that canHandle throws an {@link IllegalArgumentException} for a null context.
*/
@Test(expectedExceptions = IllegalArgumentException.class)
public void testCanHandleDoesNotAcceptNull() {
createAuthorizationProcessor().canHandle((Object[]) null);
}
/**
* Tests that canHandle yields false for any object other than {@link HttpServletRequest}.
*/
@Test()
public void testCanHandleDoesNotAcceptUnhandledContext() {
assertFalse(createAuthorizationProcessor().canHandle(new Object()));
}
/**
* Tests that updated does not throw an exception for a correct configuration.
*/
@Test()
public void testUpdatedDoesAcceptCorrectProperties() throws ConfigurationException {
final String lookupKey = "anyKey";
final String matchPolicy = "cn";
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_LOOKUPKEY, lookupKey);
props.put(PROPERTY_USERNAME_MATCH_POLICY, matchPolicy);
props.put(PROPERTY_VERIFY_CERT_VALIDITY, "true");
ClientCertAuthenticationProcessor processor = createAuthorizationProcessor();
processor.updated(props);
X509Certificate[] certificateChain = createValidCertificateChain("alice");
// Test whether we can use the new properties...
when(m_servletRequest.getAttribute(ATTRIBUTE_X509_CERTIFICATE)).thenReturn(certificateChain);
User user = mock(User.class);
when(user.getName()).thenReturn("alice");
when(m_userAdmin.getUser(eq(lookupKey), eq("alice"))).thenReturn(user);
User result = processor.authenticate(m_userAdmin, m_servletRequest);
assertNotNull(result, "Expected a valid user to be returned!");
assertEquals(user.getName(), "alice", "Expected alice to be returned as user!");
}
/**
* Tests that updated throws an exception for missing "username match policy" property.
*/
@Test(expectedExceptions = ConfigurationException.class)
public void testUpdatedDoesNotAcceptEmptyMatchPolicy() throws ConfigurationException {
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_LOOKUPKEY, "foo");
props.put(PROPERTY_USERNAME_MATCH_POLICY, "");
props.put(PROPERTY_VERIFY_CERT_VALIDITY, "true");
createAuthorizationProcessor().updated(props);
}
/**
* Tests that updated throws an exception for missing "username lookup key" property.
*/
@Test(expectedExceptions = ConfigurationException.class)
public void testUpdatedDoesNotAcceptEmptyLookupKey() throws ConfigurationException {
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_LOOKUPKEY, "");
props.put(PROPERTY_USERNAME_MATCH_POLICY, "foo");
props.put(PROPERTY_VERIFY_CERT_VALIDITY, "true");
createAuthorizationProcessor().updated(props);
}
/**
* Tests that updated throws an exception for missing "verify cert validity" property.
*/
@Test(expectedExceptions = ConfigurationException.class)
public void testUpdatedDoesNotAcceptEmptyVerifyCertValidity() throws ConfigurationException {
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_LOOKUPKEY, "foo");
props.put(PROPERTY_USERNAME_MATCH_POLICY, "bar");
props.put(PROPERTY_VERIFY_CERT_VALIDITY, "");
createAuthorizationProcessor().updated(props);
}
/**
* Tests that updated throws an exception for missing "username match policy" property.
*/
@Test(expectedExceptions = ConfigurationException.class)
public void testUpdatedDoesNotAcceptMissingMatchPolicy() throws ConfigurationException {
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_LOOKUPKEY, "foo");
props.put(PROPERTY_VERIFY_CERT_VALIDITY, "true");
createAuthorizationProcessor().updated(props);
}
/**
* Tests that updated throws an exception for missing "user name lookup key" property.
*/
@Test(expectedExceptions = ConfigurationException.class)
public void testUpdatedDoesNotAcceptMissingUsernameLookupKey() throws ConfigurationException {
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_MATCH_POLICY, "foo");
props.put(PROPERTY_VERIFY_CERT_VALIDITY, "true");
createAuthorizationProcessor().updated(props);
}
/**
* Tests that updated throws an exception for missing "verify cert validity" property.
*/
@Test(expectedExceptions = ConfigurationException.class)
public void testUpdatedDoesNotAcceptMissingVerifyCertValidity() throws ConfigurationException {
Dictionary<String, Object> props = new Hashtable<>();
props.put(PROPERTY_USERNAME_LOOKUPKEY, "foo");
props.put(PROPERTY_USERNAME_MATCH_POLICY, "foo");
createAuthorizationProcessor().updated(props);
}
/**
* Creates a new {@link ClientCertAuthenticationProcessor} instance.
*
* @return a new authentication processor instance, never <code>null</code>.
*/
private ClientCertAuthenticationProcessor createAuthorizationProcessor() {
return new ClientCertAuthenticationProcessor(m_log);
}
/**
* Creates a new certificate.
*
* @param name
* the (common) name of the certificate;
* @param notBefore
* the date after which the certificate is valid;
* @param notAfter
* the date until the certificate is valid.
* @return a new {@link X509Certificate}, never <code>null</code>.
*/
private X509Certificate createCertificate(String name, final Date notBefore, final Date notAfter) {
KeyPair keypair = m_keystore.generateKeyPair();
return m_keystore.createCertificate("cn=" + name, notBefore, notAfter, keypair.getPublic());
}
/**
* Creates a new (valid) chain with certificate(s) valid from yesterday until tomorrow.
*
* @param dns
* the distinguished names of the certificates in the returned chain.
* @return a new chain with {@link X509Certificate}s, never <code>null</code>.
*/
private X509Certificate[] createValidCertificateChainWithDN(String... dns) {
X509Certificate[] result = new X509Certificate[dns.length];
X500Principal signerDN = m_keystore.getCA_DN();
KeyPair signerKeyPair = m_keystore.getCA_KeyPair();
for (int i = 0; i < result.length; i++) {
KeyPair certKeyPair = m_keystore.generateKeyPair();
String dn = dns[i];
int idx = result.length - i - 1;
result[idx] = m_keystore.createCertificate(signerDN, signerKeyPair.getPrivate(), dn, yesterday(), tomorrow(), certKeyPair.getPublic());
signerDN = result[idx].getSubjectX500Principal();
signerKeyPair = certKeyPair;
}
return result;
}
/**
* Creates a new (valid) certificate valid from yesterday until tomorrow.
*
* @param name
* the (common) name of the certificate;
* @return a new {@link X509Certificate}, never <code>null</code>.
*/
private X509Certificate[] createValidCertificateChain(String name) {
X509Certificate[] result = new X509Certificate[1];
result[0] = createCertificate(name, yesterday(), tomorrow());
return result;
}
/**
* Creates a new (expired) certificate valid from two days ago until yesterday.
*
* @param name
* the (common) name of the certificate;
* @return a new {@link X509Certificate}, never <code>null</code>.
*/
private X509Certificate[] createExpiredCertificateChain(String name) {
X509Certificate[] result = new X509Certificate[1];
result[0] = createCertificate(name, dayBeforeYesterday(), yesterday());
return result;
}
/**
* Creates a new (not yet valid) certificate valid from tomorrow until the day after tomorrow.
*
* @param name
* the (common) name of the certificate;
* @return a new {@link X509Certificate}, never <code>null</code>.
*/
private X509Certificate[] createNotValidCertificateChain(String name) {
X509Certificate[] result = new X509Certificate[1];
result[0] = createCertificate(name, tomorrow(), dayAfterTomorrow());
return result;
}
}