You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by el...@apache.org on 2019/05/09 22:44:01 UTC

[directory-server] branch master updated: Applied patch for PR # 15 ("Provide SASL EXTERNAL authentication using client certificates")

This is an automated email from the ASF dual-hosted git repository.

elecharny pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/directory-server.git


The following commit(s) were added to refs/heads/master by this push:
     new 030638e  Applied patch for PR # 15 ("Provide SASL EXTERNAL authentication using client certificates")
030638e is described below

commit 030638e8dc7304a3cf5295b10ff1e554a478c4f9
Author: emmanuel lecharny <el...@apache.org>
AuthorDate: Fri May 10 00:43:57 2019 +0200

    Applied patch for PR # 15 ("Provide SASL EXTERNAL authentication using
    client certificates")
---
 core/pom.xml                                       |   2 +
 .../server/core/security/TlsKeyGenerator.java      |  12 +-
 .../certificate/CertificateMechanismHandler.java   |  79 ++++++++
 .../external/certificate/ExternalSaslServer.java   | 184 ++++++++++++++++++
 .../server/annotations/CreateTransport.java        |   3 +
 .../apache/directory/server/annotations/Sasl.java  |   4 +-
 .../server/factory/ServerAnnotationProcessor.java  |   2 +
 server-config/src/test/resources/ldapServer.ldif   |   7 +
 .../ClientCertificateAuthenticationIT.java         | 206 +++++++++++++++++++++
 .../ssl/ClientCertificateSslSocketFactory.java     | 112 +++++++++++
 10 files changed, 608 insertions(+), 3 deletions(-)

diff --git a/core/pom.xml b/core/pom.xml
index 57a03bb..1fb881b 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -258,6 +258,8 @@
                 org.apache.directory.server.i18n;version=${project.version},
                 org.bouncycastle.jce.provider;version=${bcprov.version},
                 org.bouncycastle.x509;version=${bcprov.version},
+                org.bouncycastle.asn1;version=${bcprov.version},
+                org.bouncycastle.asn1.x509;version=${bcprov.version},
                 org.slf4j;version=${slf4j.api.bundleversion}
             </Import-Package>
           </instructions>
diff --git a/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java b/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java
index 9404cf6..ec5d23b 100644
--- a/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java
+++ b/core/src/main/java/org/apache/directory/server/core/security/TlsKeyGenerator.java
@@ -47,8 +47,12 @@ import org.apache.directory.api.ldap.model.entry.Attribute;
 import org.apache.directory.api.ldap.model.entry.Entry;
 import org.apache.directory.api.ldap.model.exception.LdapException;
 import org.apache.directory.server.i18n.I18n;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.x509.X509V1CertificateGenerator;
+import org.bouncycastle.x509.X509V3CertificateGenerator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -297,7 +301,7 @@ public final class TlsKeyGenerator
         // Generate the self-signed certificate
         BigInteger serialNumber = BigInteger.valueOf( System.currentTimeMillis() );
 
-        X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+        X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
         X500Principal issuerName = new X500Principal( issuerDN );
         X500Principal subjectName = new X500Principal( subjectDN );
 
@@ -308,6 +312,10 @@ public final class TlsKeyGenerator
         certGen.setSubjectDN( subjectName );
         certGen.setPublicKey( publicKey );
         certGen.setSignatureAlgorithm( "SHA256With" + keyAlgo );
+        certGen.addExtension( Extension.basicConstraints, false, new BasicConstraints( false ) );
+        certGen.addExtension( Extension.extendedKeyUsage, true, new ExtendedKeyUsage( 
+            new KeyPurposeId[] { KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth } ) );
+
         
 
         try
diff --git a/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/CertificateMechanismHandler.java b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/CertificateMechanismHandler.java
new file mode 100644
index 0000000..9d73921
--- /dev/null
+++ b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/CertificateMechanismHandler.java
@@ -0,0 +1,79 @@
+/*
+ *  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.certificate;
+
+import org.apache.directory.api.ldap.model.message.BindRequest;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.ldap.LdapSession;
+import org.apache.directory.server.ldap.handlers.sasl.AbstractMechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.SaslConstants;
+
+import javax.security.sasl.SaslServer;
+
+/**
+ * The External Sasl mechanism handler which to authenticate user by client certificate (ssl).
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class CertificateMechanismHandler extends AbstractMechanismHandler
+{
+    public SaslServer handleMechanism( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
+    {
+        SaslServer ss = ( SaslServer ) ldapSession.getSaslProperty( SaslConstants.SASL_SERVER );
+
+        if ( ss == null )
+        {
+            String saslHost = ldapSession.getLdapServer().getSaslHost();
+            String userBaseDn = ldapSession.getLdapServer().getSearchBaseDn();
+            ldapSession.putSaslProperty( SaslConstants.SASL_HOST, saslHost );
+            ldapSession.putSaslProperty( SaslConstants.SASL_USER_BASE_DN, userBaseDn );
+
+            CoreSession adminSession = ldapSession.getLdapServer().getDirectoryService().getAdminSession();
+
+            ss = new ExternalSaslServer( ldapSession, adminSession, bindRequest );
+
+            ldapSession.putSaslProperty( SaslConstants.SASL_SERVER, ss );
+        }
+
+        return ss;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public void init( LdapSession ldapSession )
+    {
+        // Store the host in the ldap session
+        String saslHost = ldapSession.getLdapServer().getSaslHost();
+        ldapSession.putSaslProperty( SaslConstants.SASL_HOST, saslHost );
+    }
+
+
+    /**
+     * Remove the SaslServer and Mechanism property.
+     * 
+     * @param ldapSession the Ldapsession instance
+     */
+    public void cleanup( LdapSession ldapSession )
+    {
+        ldapSession.clearSaslProperties();
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..48a5e0d
--- /dev/null
+++ b/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/sasl/external/certificate/ExternalSaslServer.java
@@ -0,0 +1,184 @@
+/*
+ *  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.certificate;
+
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
+import org.apache.directory.api.ldap.model.constants.SchemaConstants;
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.message.BindRequest;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.util.Strings;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.LdapPrincipal;
+import org.apache.directory.server.core.api.OperationEnum;
+import org.apache.directory.server.core.api.OperationManager;
+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;
+import org.apache.mina.filter.ssl.SslFilter;
+
+import javax.naming.Context;
+import javax.net.ssl.SSLSession;
+import javax.security.sasl.SaslException;
+import java.security.cert.Certificate;
+
+
+/**
+ * A SaslServer implementation for certificate based SASL EXTERNAL mechanism.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public final class ExternalSaslServer extends AbstractSaslServer
+{
+    /**
+     * The possible states for the negotiation of a EXTERNAL mechanism.
+     */
+    private enum NegotiationState
+    {
+        INITIALIZED, // Negotiation has just started
+        COMPLETED // The user/password have been received
+    }
+
+    /** The current negotiation state */
+    private NegotiationState state;
+
+    /**
+     *
+     * Creates a new instance of ExternalSaslServer.
+     *
+     * @param ldapSession The associated LdapSession instance
+     * @param adminSession The Administrator session
+     * @param bindRequest The associated BindRequest object
+     */
+    ExternalSaslServer( LdapSession ldapSession, CoreSession adminSession, BindRequest bindRequest )
+    {
+        super( ldapSession, adminSession, bindRequest );
+        state = NegotiationState.INITIALIZED;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getMechanismName()
+    {
+        return SupportedSaslMechanisms.EXTERNAL;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public byte[] evaluateResponse( byte[] initialResponse ) throws SaslException
+    {
+        try
+        {
+            SSLSession sslSession = ( SSLSession ) getLdapSession().getIoSession().getAttribute( SslFilter.SSL_SESSION );
+            Certificate[] peerCertificates = sslSession.getPeerCertificates();
+
+            if ( null == peerCertificates || 1 > peerCertificates.length )
+            {
+                throw new SaslException( "No peer certificate provided - cancel bind." );
+            }
+
+            getLdapSession().setCoreSession( authenticate( peerCertificates[0] ) );
+            state = NegotiationState.COMPLETED;
+        }
+        catch ( Exception e )
+        {
+            throw new SaslException( "Error authentication using client certificate: " + ExceptionUtils.getStackTrace( e ), e );
+        }
+        
+        return Strings.EMPTY_BYTES;
+    }
+
+
+    /**
+     * Provides {@code true} if negationstate is {@link NegotiationState#COMPLETED}
+     *
+     * @return {@code true} if completed, otherwise {@code false}
+     */
+    public boolean isComplete()
+    {
+        return state == NegotiationState.COMPLETED;
+    }
+
+
+    /**
+     * Try to authenticate the user against the underlying LDAP server.
+     * We identify the user using the provided peercertificate.
+     */
+    private CoreSession authenticate( Certificate peerCertificate ) throws Exception
+    {
+        LdapSession ldapSession = getLdapSession();
+        CoreSession adminSession = getAdminSession();
+        DirectoryService directoryService = adminSession.getDirectoryService();
+        LdapServer ldapServer = ldapSession.getLdapServer();
+        OperationManager operationManager = directoryService.getOperationManager();
+
+        // find user by userCertificate
+        EqualityNode<String> filter = new EqualityNode<>(
+                directoryService.getSchemaManager().getAttributeType( SchemaConstants.USER_CERTIFICATE_AT ),
+                new Value( peerCertificate.getEncoded() ) );
+
+        SearchOperationContext searchContext = new SearchOperationContext( directoryService.getAdminSession() );
+        searchContext.setDn( directoryService.getDnFactory().create( ldapServer.getSearchBaseDn() ) );
+        searchContext.setScope( SearchScope.SUBTREE );
+        searchContext.setFilter( filter );
+        searchContext.setSizeLimit( 1 );
+        searchContext.setNoAttributes( true );
+
+        try ( EntryFilteringCursor cursor = operationManager.search( searchContext ) )
+        {
+            if ( cursor.next() )
+            {
+                Entry entry = cursor.get();
+
+                BindOperationContext bindContext = new BindOperationContext( ldapSession.getCoreSession() );
+                bindContext.setDn( entry.getDn() );
+                bindContext.setSaslMechanism( getMechanismName() );
+                bindContext.setSaslAuthId( getBindRequest().getName() );
+                bindContext.setIoSession( ldapSession.getIoSession() );
+                bindContext.setInterceptors( directoryService.getInterceptors( OperationEnum.BIND ) );
+
+                operationManager.bind( bindContext );
+
+                ldapSession.putSaslProperty( SaslConstants.SASL_AUTHENT_USER, new LdapPrincipal( directoryService.getSchemaManager(),
+                        entry.getDn(), AuthenticationLevel.STRONG ) );
+                getLdapSession().putSaslProperty( Context.SECURITY_PRINCIPAL, getBindRequest().getName() );
+
+                return bindContext.getSession();
+            }
+
+            throw new LdapAuthenticationException( "Cannot authenticate user cert=" + peerCertificate );
+        }
+    }
+}
\ No newline at end of file
diff --git a/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java b/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java
index b3fe0cb..05c7f44 100644
--- a/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java
+++ b/server-annotations/src/main/java/org/apache/directory/server/annotations/CreateTransport.java
@@ -68,4 +68,7 @@ public @interface CreateTransport
 
     /** @return The number of threads to use. Default to 3*/
     int nbThreads() default 3;
+
+    /** @return A flag to tell if the transport should ask for client certificate. Default to false */
+    boolean clientAuth() default false;
 }
\ No newline at end of file
diff --git a/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java b/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java
index bd17688..9e99493 100644
--- a/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java
+++ b/server-annotations/src/main/java/org/apache/directory/server/annotations/Sasl.java
@@ -30,6 +30,7 @@ import java.lang.annotation.Target;
 import org.apache.directory.server.ldap.handlers.sasl.SimpleMechanismHandler;
 import org.apache.directory.server.ldap.handlers.sasl.cramMD5.CramMd5MechanismHandler;
 import org.apache.directory.server.ldap.handlers.sasl.digestMD5.DigestMd5MechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.external.certificate.CertificateMechanismHandler;
 import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
 import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
 
@@ -77,6 +78,7 @@ public @interface Sasl
             CramMd5MechanismHandler.class,
             DigestMd5MechanismHandler.class,
             GssapiMechanismHandler.class,
-            NtlmMechanismHandler.class
+            NtlmMechanismHandler.class,
+            CertificateMechanismHandler.class
     };
 }
\ No newline at end of file
diff --git a/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java b/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java
index 7b17dd3..8e813b0 100644
--- a/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java
+++ b/server-annotations/src/main/java/org/apache/directory/server/factory/ServerAnnotationProcessor.java
@@ -490,6 +490,7 @@ public final class ServerAnnotationProcessor
         int nbThreads = transportBuilder.nbThreads();
         int backlog = transportBuilder.backlog();
         String address = transportBuilder.address();
+        boolean clientAuth = transportBuilder.clientAuth();
         
         if ( Strings.isEmpty( address ) )
         {
@@ -517,6 +518,7 @@ public final class ServerAnnotationProcessor
         {
             Transport tcp = new TcpTransport( address, port, nbThreads, backlog );
             tcp.setEnableSSL( true );
+            ( ( TcpTransport ) tcp ).setWantClientAuth( clientAuth );
             return Collections.singletonList( tcp );
         }
         else if ( protocol.equalsIgnoreCase( "UDP" ) )
diff --git a/server-config/src/test/resources/ldapServer.ldif b/server-config/src/test/resources/ldapServer.ldif
index 69a161d..4d3c625 100644
--- a/server-config/src/test/resources/ldapServer.ldif
+++ b/server-config/src/test/resources/ldapServer.ldif
@@ -153,6 +153,13 @@ objectclass: top
 ads-saslMechName: SIMPLE
 ads-enabled: true
 
+dn: ads-saslMechName=external,ou=saslmechhandlers,ads-serverId=ldapServer,ou=servers,ads-directoryServiceId=default,ou=config
+ads-saslMechClassName: org.apache.directory.server.ldap.handlers.sasl.external.certificate.CertificateMechanismHandler
+objectclass: ads-saslMechHandler
+objectclass: top
+ads-saslMechName: EXTERNAL
+ads-enabled: true
+
 dn: ou=replConsumers,ads-serverId=ldapServer,ou=servers,ads-directoryServiceId=default,ou=config
 objectClass: organizationalUnit
 objectClass: top
diff --git a/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAuthenticationIT.java b/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAuthenticationIT.java
new file mode 100644
index 0000000..50d175f
--- /dev/null
+++ b/server-integ/src/test/java/org/apache/directory/server/ldap/handlers/sasl/external/ClientCertificateAuthenticationIT.java
@@ -0,0 +1,206 @@
+/*
+ *  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 a testuser which is also used for ldap connection.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+@RunWith(FrameworkRunner.class)
+@CreateDS(allowAnonAccess = true, name = "ClientCertificateAuthenticationIT-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 ClientCertificateAuthenticationIT extends AbstractLdapTestUnit
+{
+
+    private Dn authenticationUserDn;
+
+    /**
+     * Setup the test, prepare certificate and testuser
+     * @throws Exception on any error
+     */
+    @Before
+    public void installKeyStoreWithCertificate() throws Exception
+        {
+            authenticationUserDn = new Dn("uid=testsubject,ou=users,dc=example,dc=com");
+
+            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 );
+
+            // prepare socket factory to provide client certificate
+            try ( ByteArrayInputStream in = new ByteArrayInputStream( TlsKeyGenerator.getCertificate( entry ).getEncoded() ) )
+            {
+                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( new FileOutputStream( ClientCertificateSslSocketFactory.ksFile ), ClientCertificateSslSocketFactory.ksPassword );
+            }
+
+            // set certificte to testuser
+            Modification mod = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE,
+                TlsKeyGenerator.USER_CERTIFICATE_AT, entry.get( TlsKeyGenerator.USER_CERTIFICATE_AT ).getBytes() );
+            getLdapServer().getDirectoryService().getAdminSession().modify(new Dn("uid=testsubject,ou=users,dc=example,dc=com"), 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 testExternalClientCertificateAuthentication() throws Exception
+    {
+        // create a new secure connection
+        Hashtable<Object, Object> 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<SearchResult> 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();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/server-integ/src/test/java/org/apache/directory/server/ssl/ClientCertificateSslSocketFactory.java b/server-integ/src/test/java/org/apache/directory/server/ssl/ClientCertificateSslSocketFactory.java
new file mode 100644
index 0000000..880f2c8
--- /dev/null
+++ b/server-integ/src/test/java/org/apache/directory/server/ssl/ClientCertificateSslSocketFactory.java
@@ -0,0 +1,112 @@
+/*
+ *  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.ssl;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+
+
+/**
+ * Factory to create a SSLContext providing a client certificate.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class ClientCertificateSslSocketFactory extends SocketFactory
+{
+    private static SocketFactory customSSLSocketFactory;
+
+    public static File ksFile = new File("target/clientkeystore.jks");;
+    public static char[] ksPassword = "changeit".toCharArray();
+
+    {
+        try {
+            KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType() );
+
+            try( InputStream keyInput = new FileInputStream( ksFile.getAbsoluteFile() ) )
+            {
+                keyStore.load( keyInput, ksPassword );
+            }
+
+            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( "SunX509" );
+            keyManagerFactory.init( keyStore, ksPassword );
+
+            SSLContext context = SSLContext.getInstance( "TLS" );
+            context.init( keyManagerFactory.getKeyManagers(), BogusTrustManagerFactory.X509_MANAGERS, new SecureRandom() );
+
+            customSSLSocketFactory = context.getSocketFactory();
+        }
+        catch ( Exception e )
+        {
+            throw new RuntimeException( "Error initializing ClientCertificateSslSocketFactory ", e );
+        }
+    }
+
+
+    /**
+     * This method is needed. It is called by the LDAP Context to create the connection
+     *
+     * @see SocketFactory#getDefault()
+     */
+    @SuppressWarnings("unused")
+    public static SocketFactory getDefault()
+    {
+        return new ClientCertificateSslSocketFactory();
+    }
+
+    /**
+     * @see SocketFactory#createSocket(String, int)
+     */
+    public Socket createSocket(String arg0, int arg1) throws IOException
+    {
+        return customSSLSocketFactory.createSocket(arg0, arg1);
+    }
+
+    /**
+     * @see SocketFactory#createSocket(InetAddress, int)
+     */
+    public Socket createSocket(InetAddress arg0, int arg1) throws IOException
+    {
+        return customSSLSocketFactory.createSocket(arg0, arg1);
+    }
+
+    /**
+     * @see SocketFactory#createSocket(String, int, InetAddress, int)
+     */
+    public Socket createSocket(String arg0, int arg1, InetAddress arg2, int arg3) throws IOException
+    {
+        return customSSLSocketFactory.createSocket(arg0, arg1, arg2, arg3);
+    }
+
+    /**
+     * @see SocketFactory#createSocket(InetAddress, int, InetAddress, int)
+     */
+    public Socket createSocket(InetAddress arg0, int arg1, InetAddress arg2, int arg3) throws IOException
+    {
+        return customSSLSocketFactory.createSocket(arg0, arg1, arg2, arg3);
+    }
+}
\ No newline at end of file