diff --git a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java index 48a5e0dc22..c1526da6d3 100644 --- a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java +++ b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java @@ -39,7 +39,6 @@ import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; import org.apache.directory.server.core.api.interceptor.context.BindOperationContext; import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext; -import org.apache.directory.server.ldap.LdapServer; import org.apache.directory.server.ldap.LdapSession; import org.apache.directory.server.ldap.handlers.sasl.AbstractSaslServer; import org.apache.directory.server.ldap.handlers.sasl.SaslConstants; @@ -137,11 +136,29 @@ public boolean isComplete() * We identify the user using the provided peercertificate. */ private CoreSession authenticate( Certificate peerCertificate ) throws Exception + { + // search for client certificate from base dn + CoreSession session = searchUserWithCertificate( peerCertificate, getLdapSession().getLdapServer().getSearchBaseDn() ); + if ( session != null ) + { + return session; + } + + // search for client certificate from admin user + session = searchUserWithCertificate( peerCertificate, "uid=admin,ou=system" ); + if ( session != null ) + { + return session; + } + + throw new LdapAuthenticationException( "Cannot authenticate user cert=" + peerCertificate ); + } + + private CoreSession searchUserWithCertificate( Certificate peerCertificate, String baseDn ) throws Exception { LdapSession ldapSession = getLdapSession(); CoreSession adminSession = getAdminSession(); DirectoryService directoryService = adminSession.getDirectoryService(); - LdapServer ldapServer = ldapSession.getLdapServer(); OperationManager operationManager = directoryService.getOperationManager(); // find user by userCertificate @@ -150,7 +167,7 @@ private CoreSession authenticate( Certificate peerCertificate ) throws Exception new Value( peerCertificate.getEncoded() ) ); SearchOperationContext searchContext = new SearchOperationContext( directoryService.getAdminSession() ); - searchContext.setDn( directoryService.getDnFactory().create( ldapServer.getSearchBaseDn() ) ); + searchContext.setDn( directoryService.getDnFactory().create( baseDn ) ); searchContext.setScope( SearchScope.SUBTREE ); searchContext.setFilter( filter ); searchContext.setSizeLimit( 1 ); @@ -177,8 +194,8 @@ private CoreSession authenticate( Certificate peerCertificate ) throws Exception return bindContext.getSession(); } - - throw new LdapAuthenticationException( "Cannot authenticate user cert=" + peerCertificate ); } + + return null; } -} \ No newline at end of file +} diff --git a/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAdminAuthenticationIT.java b/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAdminAuthenticationIT.java new file mode 100644 index 0000000000..e1de7d3bb4 --- /dev/null +++ b/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAdminAuthenticationIT.java @@ -0,0 +1,208 @@ +/* + * 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.directory.server.ldap.handlers.sasl.external; + +import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms; +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.entry.DefaultModification; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.entry.Modification; +import org.apache.directory.api.ldap.model.entry.ModificationOperation; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.directory.api.util.Network; +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.annotations.SaslMechanism; +import org.apache.directory.server.core.annotations.*; +import org.apache.directory.server.core.integ.AbstractLdapTestUnit; +import org.apache.directory.server.core.integ.FrameworkRunner; +import org.apache.directory.server.core.security.TlsKeyGenerator; +import org.apache.directory.server.ldap.handlers.sasl.external.certificate.CertificateMechanismHandler; +import org.apache.directory.server.ssl.ClientCertificateSslSocketFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.naming.NamingEnumeration; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.net.InetAddress; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.util.Date; +import java.util.Hashtable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test the authentication using EXTERNAL SASL client certificate authentication. + * Stores the client certificate on the admin which is also used for ldap connection. + * + * @author Apache Directory Project + */ +@RunWith(FrameworkRunner.class) +@CreateDS(allowAnonAccess = true, name = "ClientCertificateAdminAuthenticationIT-class", + partitions = + { + @CreatePartition( + name = "example", + suffix = "dc=example,dc=com", + contextEntry = @ContextEntry( + entryLdif = + "dn: dc=example,dc=com\n" + + "dc: example\n" + + "objectClass: top\n" + + "objectClass: domain\n\n"), + indexes = + { + @CreateIndex(attribute = "objectClass"), + @CreateIndex(attribute = "dc"), + @CreateIndex(attribute = "ou") + }) + }) +@CreateLdapServer( + transports = + { + @CreateTransport(protocol = "LDAPS", clientAuth = true) + }, + saslMechanisms = + { + @SaslMechanism(name = SupportedSaslMechanisms.EXTERNAL, implClass = CertificateMechanismHandler.class) + }) +@ApplyLdifs( + { + // Entry # 1 + "dn: ou=users,dc=example,dc=com", + "objectClass: organizationalUnit", + "objectClass: top", + "ou: users\n", + + // Entry # 2 + "dn: uid=testsubject,ou=users,dc=example,dc=com", + "objectClass: inetOrgPerson", + "objectClass: organizationalPerson", + "objectClass: person", + "objectClass: top", + "uid: testsubject", + "userPassword: not_set", + "cn: Test Subject", + "sn: Subject" + }) + +public class ClientCertificateAdminAuthenticationIT extends AbstractLdapTestUnit +{ + + private Dn authenticationUserDn; + + /** + * Setup the test, prepare certificate and add userCertificate attribute for admin + * @throws Exception on any error + */ + @Before + public void installKeyStoreWithCertificate() throws Exception + { + authenticationUserDn = new Dn("uid=admin,ou=system"); + + String hostName = InetAddress.getLocalHost().getHostName(); + String issuerDn = TlsKeyGenerator.CERTIFICATE_PRINCIPAL_DN; + String subjectDn = "CN=" + hostName; + Date startDate = new Date(); + Date expiryDate = new Date( System.currentTimeMillis() + TlsKeyGenerator.YEAR_MILLIS ); + String keyAlgo = "RSA"; + int keySize = 1024; + + Entry entry = new DefaultEntry(); + TlsKeyGenerator.addKeyPair( entry, issuerDn, subjectDn, startDate, expiryDate, keyAlgo, keySize, null, false ); + + // prepare socket factory to provide client certificate + try ( + ByteArrayInputStream in = new ByteArrayInputStream( TlsKeyGenerator.getCertificate( entry ).getEncoded() ); + FileOutputStream out = new FileOutputStream( ClientCertificateSslSocketFactory.ksFile ) ) + { + CertificateFactory factory = CertificateFactory.getInstance( "X.509" ); + Certificate cert = factory.generateCertificate( in ); + KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() ); + ks.load( null, null ); + ks.setKeyEntry("apacheds", TlsKeyGenerator.getKeyPair( entry ).getPrivate(), ClientCertificateSslSocketFactory.ksPassword, new Certificate[] { cert } ); + ks.store( out, ClientCertificateSslSocketFactory.ksPassword ); + } + + // set certificte attribute to admin + Modification mod = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, + TlsKeyGenerator.USER_CERTIFICATE_AT, entry.get( TlsKeyGenerator.USER_CERTIFICATE_AT ).getBytes() ); + getLdapServer().getDirectoryService().getAdminSession().modify( authenticationUserDn, mod ); + } + + /** + * Cleanup test, remove keystore + * @throws Exception on any error + */ + @After + public void teardown() throws Exception { + if ( ClientCertificateSslSocketFactory.ksFile != null && ClientCertificateSslSocketFactory.ksFile.exists() ) + { + ClientCertificateSslSocketFactory.ksFile.delete(); + } + } + + /** + * Do just a connect and a simple search to verify if authentication works. + * The test checks the authentication user in the current ldap session. + * + * @throws Exception on any error + */ + @Test + public void testExternalClientCertificateAdminAuthentication() throws Exception + { + // create a new secure connection + Hashtable env = new Hashtable<>(); + env.put( "java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory" ); + env.put( "java.naming.provider.url", Network.ldapLoopbackUrl( getLdapServer().getPortSSL() ) ); + env.put( "java.naming.security.protocol", "ssl"); + env.put( "java.naming.ldap.factory.socket", ClientCertificateSslSocketFactory.class.getName () ); + env.put( "java.naming.security.authentication", "EXTERNAL" ); + + DirContext ctx = new InitialDirContext( env ); + try + { + String searchFilter = "(objectClass=*)"; + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope( SearchControls.OBJECT_SCOPE ); + + NamingEnumeration results = ctx.search( "dc=example,dc=com", searchFilter, searchControls ); + assertTrue( results.hasMore() ); + + assertEquals(authenticationUserDn.getName(), + getLdapServer().getLdapSessionManager().getSessions()[0].getCoreSession().getAuthenticatedPrincipal().getDn().getName()); + } + finally + { + ctx.close(); + } + } + +}