Added SASL IT tests

git-svn-id: https://svn.apache.org/repos/asf/directory/apacheds/trunk@1749718 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/ldap-client-test/src/test/java/org/apache/directory/shared/client/api/operations/bind/BogusNtlmProvider.java b/ldap-client-test/src/test/java/org/apache/directory/shared/client/api/operations/bind/BogusNtlmProvider.java
new file mode 100644
index 0000000..dc85d2c
--- /dev/null
+++ b/ldap-client-test/src/test/java/org/apache/directory/shared/client/api/operations/bind/BogusNtlmProvider.java
@@ -0,0 +1,64 @@
+/*
+ *   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.shared.client.api.operations.bind;
+
+
+import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmProvider;
+import org.apache.mina.core.session.IoSession;
+
+
+/**
+ * A fake implementation of the NtlmProvider. We can't use a real one because
+ * its license is not ASL 2.0 compatible.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class BogusNtlmProvider implements NtlmProvider
+{
+    private byte[] type1response;
+    private byte[] type3response;
+
+
+    public boolean authenticate( IoSession session, byte[] type3response ) throws Exception
+    {
+        this.type3response = type3response;
+        return true;
+    }
+
+
+    public byte[] generateChallenge( IoSession session, byte[] type1reponse ) throws Exception
+    {
+        this.type1response = type1reponse;
+        return "challenge".getBytes();
+    }
+
+
+    public byte[] getType1Response()
+    {
+        return type1response;
+    }
+
+
+    public byte[] getType3Response()
+    {
+        return type3response;
+    }
+}
diff --git a/ldap-client-test/src/test/java/org/apache/directory/shared/client/api/operations/bind/SaslBindIT.java b/ldap-client-test/src/test/java/org/apache/directory/shared/client/api/operations/bind/SaslBindIT.java
new file mode 100644
index 0000000..7a02066
--- /dev/null
+++ b/ldap-client-test/src/test/java/org/apache/directory/shared/client/api/operations/bind/SaslBindIT.java
@@ -0,0 +1,833 @@
+/*
+ *  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.shared.client.api.operations.bind;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.net.InetAddress;
+
+import org.apache.directory.api.ldap.model.constants.SaslQoP;
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+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.api.ldap.model.message.BindRequest;
+import org.apache.directory.api.ldap.model.message.BindRequestImpl;
+import org.apache.directory.api.ldap.model.message.BindResponse;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.directory.ldap.client.api.SaslCramMd5Request;
+import org.apache.directory.ldap.client.api.SaslDigestMd5Request;
+import org.apache.directory.ldap.client.api.SaslGssApiRequest;
+import org.apache.directory.server.annotations.CreateKdcServer;
+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.ApplyLdifs;
+import org.apache.directory.server.core.annotations.ContextEntry;
+import org.apache.directory.server.core.annotations.CreateDS;
+import org.apache.directory.server.core.annotations.CreateIndex;
+import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
+import org.apache.directory.server.kerberos.kdc.KerberosTestUtils;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
+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.gssapi.GssapiMechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.ntlm.NtlmMechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * An {@link AbstractServerTest} testing SASL authentication.
+ * 
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+@RunWith(FrameworkRunner.class)
+@ApplyLdifs(
+    {
+        // Entry # 1
+        "dn: ou=users,dc=example,dc=com",
+        "objectClass: organizationalUnit",
+        "objectClass: top",
+        "ou: users\n",
+
+        // Entry # 2
+        "dn: uid=hnelson,ou=users,dc=example,dc=com",
+        "objectClass: inetOrgPerson",
+        "objectClass: organizationalPerson",
+        "objectClass: person",
+        "objectClass: krb5principal",
+        "objectClass: krb5kdcentry",
+        "objectClass: top",
+        "uid: hnelson",
+        "userPassword: secret",
+        "krb5PrincipalName: hnelson@EXAMPLE.COM",
+        "krb5KeyVersionNumber: 0",
+        "cn: Horatio Nelson",
+        "sn: Nelson",
+
+        // krbtgt
+        "dn: uid=krbtgt,ou=users,dc=example,dc=com",
+        "objectClass: inetOrgPerson",
+        "objectClass: organizationalPerson",
+        "objectClass: person",
+        "objectClass: krb5principal",
+        "objectClass: krb5kdcentry",
+        "objectClass: top",
+        "uid: krbtgt",
+        "userPassword: secret",
+        "krb5PrincipalName: krbtgt/EXAMPLE.COM@EXAMPLE.COM",
+        "krb5KeyVersionNumber: 0",
+        "cn: KDC Service",
+        "sn: Service",
+
+        // ldap per host
+        "dn: uid=ldap,ou=users,dc=example,dc=com",
+        "objectClass: inetOrgPerson",
+        "objectClass: organizationalPerson",
+        "objectClass: person",
+        "objectClass: krb5principal",
+        "objectClass: krb5kdcentry",
+        "objectClass: top",
+        "uid: ldap",
+        "userPassword: randall",
+        "krb5PrincipalName: ldap/localhost@EXAMPLE.COM",
+        "krb5KeyVersionNumber: 0",
+        "cn: LDAP Service",
+        "sn: Service" })
+@CreateDS(
+    allowAnonAccess = false,
+    name = "SaslBindIT-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")
+                })
+    },
+    additionalInterceptors =
+        { KeyDerivationInterceptor.class })
+@CreateLdapServer(transports =
+    {
+        @CreateTransport(protocol = "LDAP")
+},
+    saslHost = "localhost",
+    saslPrincipal = "ldap/localhost@EXAMPLE.COM",
+    saslMechanisms =
+        {
+            @SaslMechanism(name = SupportedSaslMechanisms.PLAIN, implClass = PlainMechanismHandler.class),
+            @SaslMechanism(name = SupportedSaslMechanisms.CRAM_MD5, implClass = CramMd5MechanismHandler.class),
+            @SaslMechanism(name = SupportedSaslMechanisms.DIGEST_MD5, implClass = DigestMd5MechanismHandler.class),
+            @SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass = GssapiMechanismHandler.class),
+            @SaslMechanism(name = SupportedSaslMechanisms.NTLM, implClass = NtlmMechanismHandler.class),
+            @SaslMechanism(name = SupportedSaslMechanisms.GSS_SPNEGO, implClass = NtlmMechanismHandler.class)
+    },
+    extendedOpHandlers =
+        {
+            StoredProcedureExtendedOperationHandler.class
+    },
+    ntlmProvider = BogusNtlmProvider.class)
+@CreateKdcServer(
+    transports =
+        {
+            @CreateTransport(protocol = "UDP", port = 6088),
+            @CreateTransport(protocol = "TCP", port = 6088)
+    })
+public class SaslBindIT extends AbstractLdapTestUnit
+{
+    LdapServer ldapServer;
+    
+    @Before
+    public void init() throws Exception
+    {
+        ldapServer = getLdapServer();
+        KerberosTestUtils.fixServicePrincipalName( "ldap/" + InetAddress.getLocalHost().getHostName() + "@EXAMPLE.COM",
+            new Dn( "uid=ldap,ou=users,dc=example,dc=com" ), getLdapServer() );
+    }
+
+
+    /**
+     * Tests to make sure the server properly returns the supportedSASLMechanisms.
+     */
+    @Test
+    public void testSupportedSASLMechanisms() throws Exception
+    {
+        // We have to tell the server that it should accept anonymous
+        // auth, because we are reading the rootDSE
+        ldapServer.getDirectoryService().setAllowAnonymousAccess( true );
+
+        // Point on rootDSE
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        Entry rootDn = connection.lookup( "", "supportedSASLMechanisms" );
+        Attribute supportedSASLMechanisms = rootDn.get( "supportedSASLMechanisms" );
+
+        assertEquals( 6, supportedSASLMechanisms.size() );
+        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.GSSAPI ) );
+        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.DIGEST_MD5 ) );
+        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.CRAM_MD5 ) );
+        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.NTLM ) );
+        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.PLAIN ) );
+        assertTrue( supportedSASLMechanisms.contains( SupportedSaslMechanisms.GSS_SPNEGO ) );
+        
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure PLAIN-binds works
+     */
+    @Test
+    public void testSaslBindPLAIN() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        BindResponse resp = connection.bindSaslPlain( "hnelson", "secret" );
+        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+        Entry entry = connection.lookup( userDn );
+        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+        connection.close();
+
+        // Try to bind with a wrong user
+        resp = connection.bindSaslPlain( "hnelsom", "secret" );
+        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
+
+        // Try to bind with a wrong password
+        resp = connection.bindSaslPlain( "hnelson", "secres" );
+        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
+        
+        connection.close();
+    }
+
+
+    /**
+     * Test a SASL bind with an empty mechanism
+     */
+    @Test
+    @Ignore("Activate and fix when DIRAPI-36 (Provide a SaslBindRequest extending BindRequest that can be used in LdapConnection.bind(...) method) is solved")
+    public void testSaslBindNoMech() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+        BindRequest bindReq = new BindRequestImpl();
+        bindReq.setCredentials( "secret" );
+        bindReq.setDn( userDn );
+        bindReq.setSaslMechanism( "" ); // invalid mechanism
+        bindReq.setSimple( false );
+
+        try
+        {
+            connection.bind( bindReq );
+            fail();
+        }
+        catch ( LdapException le )
+        {
+            //expected
+        }
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure CRAM-MD5 binds below the RootDSE work.
+     */
+    @Test
+    public void testSaslCramMd5Bind() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslCramMd5Request request = new SaslCramMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+        Entry entry = connection.lookup( userDn );
+        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure CRAM-MD5 binds below the RootDSE fail if the password is bad.
+     */
+    @Test
+    public void testSaslCramMd5BindBadPassword() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslCramMd5Request request = new SaslCramMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "badsecret" );
+
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work.
+     */
+    @Test
+    public void testSaslDigestMd5Bind() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslDigestMd5Request request = new SaslDigestMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+        Entry entry = connection.lookup( userDn );
+        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth'.
+     */
+    @Test
+    public void testSaslDigestMd5BindSaslQoPAuth() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslDigestMd5Request request = new SaslDigestMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+        request.setQualityOfProtection( SaslQoP.AUTH );
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+        Entry entry = connection.lookup( userDn );
+        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth-int'.
+     */
+    @Test
+    @Ignore
+    public void testSaslDigestMd5BindSaslQoPAuthInt() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslDigestMd5Request request = new SaslDigestMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+        request.setQualityOfProtection( SaslQoP.AUTH_INT );
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+        Entry entry = connection.lookup( userDn );
+        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE work with
+     * SASL Quality of Protection set to 'auth-conf'.
+     */
+    @Test
+    @Ignore
+    public void testSaslDigestMd5BindSaslQoPAuthConf() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslDigestMd5Request request = new SaslDigestMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+        request.setQualityOfProtection( SaslQoP.AUTH_CONF );
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+        Entry entry = connection.lookup( userDn );
+        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the realm is bad.
+     */
+    @Test
+    public void testSaslDigestMd5BindBadRealm() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslDigestMd5Request request = new SaslDigestMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+        request.setRealmName( "badrealm.com" );
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the password is bad.
+     */
+    @Test
+    public void testSaslDigestMd5BindBadPassword() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslDigestMd5Request request = new SaslDigestMd5Request();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "badsecret" );
+        request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, resp.getLdapResult().getResultCode() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure GSS-API binds below the RootDSE work.
+     */
+    @Test
+    public void testSaslGssApiBind() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        kdcServer.getConfig().setPaEncTimestampRequired( false );
+
+        SaslGssApiRequest request = new SaslGssApiRequest();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+        request.setRealmName( ldapServer.getSaslRealms().get( 0 ).toUpperCase() );
+        request.setKdcHost( InetAddress.getLocalHost().getHostName() );
+        request.setKdcPort( 6088 );
+        BindResponse resp = connection.bind( request );
+        assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+
+        Entry entry = connection.lookup( userDn );
+        assertEquals( "hnelson", entry.get( "uid" ).getString() );
+
+        connection.close();
+    }
+
+
+    /**
+     * Tests to make sure GSS-API binds below the RootDSE fail if the realm is bad.
+     */
+    @Test
+    public void testSaslGssApiBindBadRealm() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslGssApiRequest request = new SaslGssApiRequest();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "secret" );
+        request.setRealmName( "badrealm.com" );
+        request.setKdcHost( InetAddress.getLocalHost().getHostName() );
+        request.setKdcPort( 6088 );
+        
+        try
+        {
+            connection.bind( request );
+        }
+        catch ( Exception e )
+        {
+            assertTrue( e instanceof LdapException );
+        }
+        finally
+        {
+            connection.close();
+        }
+    }
+
+
+    /**
+     * Tests to make sure GSS-API binds below the RootDSE fail if the password is bad.
+     */
+    @Test
+    public void testSaslGssApiBindBadPassword() throws Exception
+    {
+        String hostName = InetAddress.getLoopbackAddress().getHostName();
+        LdapNetworkConnection connection = new LdapNetworkConnection( hostName, ldapServer.getPort() );
+        connection.connect();
+
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        SaslGssApiRequest request = new SaslGssApiRequest();
+        request.setUsername( userDn.getRdn().getValue() );
+        request.setCredentials( "badsecret" );
+        request.setRealmName( ldapServer.getSaslRealms().get( 0 ).toUpperCase() );
+        request.setKdcHost( InetAddress.getLocalHost().getHostName() );
+        request.setKdcPort( 6088 );
+        
+        try
+        {
+            connection.bind( request );
+        }
+        catch ( Exception e )
+        {
+            assertTrue( e instanceof LdapException );
+        }
+        finally
+        {
+            connection.close();
+        }
+    }
+
+
+    /**
+     * Tests that the plumbing for NTLM bind works.
+     *
+    @Test
+    public void testNtlmBind() throws Exception
+    {
+        BogusNtlmProvider provider = getNtlmProviderUsingReflection();
+
+        NtlmSaslBindClient client = new NtlmSaslBindClient( SupportedSaslMechanisms.NTLM );
+        BindResponse type2response = client.bindType1( "type1_test".getBytes() );
+        assertEquals( 1, type2response.getMessageId() );
+        assertEquals( ResultCodeEnum.SASL_BIND_IN_PROGRESS, type2response.getLdapResult().getResultCode() );
+        assertTrue( ArrayUtils.isEquals( "type1_test".getBytes(), provider.getType1Response() ) );
+        assertTrue( ArrayUtils.isEquals( "challenge".getBytes(), type2response.getServerSaslCreds() ) );
+
+        BindResponse finalResponse = client.bindType3( "type3_test".getBytes() );
+        assertEquals( 2, finalResponse.getMessageId() );
+        assertEquals( ResultCodeEnum.SUCCESS, finalResponse.getLdapResult().getResultCode() );
+        assertTrue( ArrayUtils.isEquals( "type3_test".getBytes(), provider.getType3Response() ) );
+    }
+
+
+    /**
+     * Tests that the plumbing for NTLM bind works.
+     *
+    @Test
+    public void testGssSpnegoBind() throws Exception
+    {
+        BogusNtlmProvider provider = new BogusNtlmProvider();
+
+        // the provider configured in @CreateLdapServer only sets for the NTLM mechanism
+        // but we use the same NtlmMechanismHandler class for GSS_SPNEGO too but this is a separate
+        // instance, so we need to set the provider in the NtlmMechanismHandler instance of GSS_SPNEGO mechanism
+        NtlmMechanismHandler ntlmHandler = ( NtlmMechanismHandler ) getLdapServer().getSaslMechanismHandlers().get(
+            SupportedSaslMechanisms.GSS_SPNEGO );
+        ntlmHandler.setNtlmProvider( provider );
+
+        NtlmSaslBindClient client = new NtlmSaslBindClient( SupportedSaslMechanisms.GSS_SPNEGO );
+        BindResponse type2response = client.bindType1( "type1_test".getBytes() );
+        assertEquals( 1, type2response.getMessageId() );
+        assertEquals( ResultCodeEnum.SASL_BIND_IN_PROGRESS, type2response.getLdapResult().getResultCode() );
+        assertTrue( ArrayUtils.isEquals( "type1_test".getBytes(), provider.getType1Response() ) );
+        assertTrue( ArrayUtils.isEquals( "challenge".getBytes(), type2response.getServerSaslCreds() ) );
+
+        BindResponse finalResponse = client.bindType3( "type3_test".getBytes() );
+        assertEquals( 2, finalResponse.getMessageId() );
+        assertEquals( ResultCodeEnum.SUCCESS, finalResponse.getLdapResult().getResultCode() );
+        assertTrue( ArrayUtils.isEquals( "type3_test".getBytes(), provider.getType3Response() ) );
+    }
+
+
+    /**
+     * Test for DIRAPI-30 (Sporadic NullPointerException during SASL bind).
+     * Tests multiple connect/bind/unbind/disconnect.
+     *
+    @Ignore("Activate when DIRAPI-30 is solved")
+    @Test
+    public void testSequentialBinds() throws Exception
+    {
+        LdapNetworkConnection connection;
+        BindResponse resp;
+        Entry entry;
+        Dn userDn = new Dn( "uid=hnelson,ou=users,dc=example,dc=com" );
+
+        for ( int i = 0; i < 1000; i++ )
+        {
+            System.out.println( "try " + i );
+
+            // Digest-MD5
+            connection = new LdapNetworkConnection( InetAddress.getLocalHost().getHostName(), ldapServer.getPort() );
+            SaslDigestMd5Request digetDigestMd5Request = new SaslDigestMd5Request();
+            digetDigestMd5Request.setUsername( userDn.getRdn().getValue() );
+            digetDigestMd5Request.setCredentials( "secret" );
+            digetDigestMd5Request.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            resp = connection.bind( digetDigestMd5Request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+            entry = connection.lookup( userDn );
+            assertEquals( "hnelson", entry.get( "uid" ).getString() );
+            connection.close();
+
+            // Cram-MD5
+            connection = new LdapNetworkConnection( InetAddress.getLocalHost().getHostName(), ldapServer.getPort() );
+            SaslCramMd5Request cramMd5Request = new SaslCramMd5Request();
+            cramMd5Request.setUsername( userDn.getRdn().getValue() );
+            cramMd5Request.setCredentials( "secret" );
+            resp = connection.bind( cramMd5Request );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+            entry = connection.lookup( userDn );
+            assertEquals( "hnelson", entry.get( "uid" ).getString() );
+            connection.close();
+
+            // GSSAPI
+            connection = new LdapNetworkConnection( InetAddress.getLocalHost().getHostName(), ldapServer.getPort() );
+            SaslGssApiRequest gssApiRequest = new SaslGssApiRequest();
+            gssApiRequest.setUsername( userDn.getRdn().getValue() );
+            gssApiRequest.setCredentials( "secret" );
+            gssApiRequest.setRealmName( ldapServer.getSaslRealms().get( 0 ) );
+            gssApiRequest.setKdcHost( InetAddress.getLocalHost().getHostName() );
+            gssApiRequest.setKdcPort( 6088 );
+            resp = connection.bind( gssApiRequest );
+            assertEquals( ResultCodeEnum.SUCCESS, resp.getLdapResult().getResultCode() );
+            entry = connection.lookup( userDn );
+            assertEquals( "hnelson", entry.get( "uid" ).getString() );
+            connection.close();
+        }
+    }
+
+    /**
+     * A NTLM client
+     *
+    class NtlmSaslBindClient extends SocketClient
+    {
+        private final Logger LOG = LoggerFactory.getLogger( NtlmSaslBindClient.class );
+
+        private final String mechanism;
+
+
+        NtlmSaslBindClient( String mechanism ) throws Exception
+        {
+            this.mechanism = mechanism;
+            setDefaultPort( getLdapServer().getPort() );
+            connect( InetAddress.getLocalHost().getHostName(), getLdapServer().getPort() );
+            setTcpNoDelay( false );
+
+            LOG.debug( "isConnected() = {}", isConnected() );
+            LOG.debug( "LocalPort     = {}", getLocalPort() );
+            LOG.debug( "LocalAddress  = {}", getLocalAddress() );
+            LOG.debug( "RemotePort    = {}", getRemotePort() );
+            LOG.debug( "RemoteAddress = {}", getRemoteAddress() );
+        }
+
+
+        BindResponse bindType1( byte[] type1response ) throws Exception
+        {
+            if ( !isConnected() )
+            {
+                throw new IllegalStateException( "Client is not connected." );
+            }
+
+            // Setup the bind request
+            BindRequestImpl request = new BindRequestImpl();
+            request.setMessageId( 1 );
+            request.setDn( new Dn( "uid=admin,ou=system" ) );
+            request.setSimple( false );
+            request.setCredentials( type1response );
+            request.setSaslMechanism( mechanism );
+            request.setVersion3( true );
+
+            // Setup the ASN1 Encoder and Decoder
+            LdapDecoder decoder = new LdapDecoder();
+
+            // Send encoded request to server
+            LdapEncoder encoder = new LdapEncoder( getLdapServer().getDirectoryService().getLdapCodecService() );
+            ByteBuffer bb = encoder.encodeMessage( request );
+
+            bb.flip();
+
+            _output_.write( bb.array() );
+            _output_.flush();
+
+            while ( _input_.available() <= 0 )
+            {
+                Thread.sleep( 100 );
+            }
+
+            // Retrieve the response back from server to my last request.
+            LdapMessageContainer<MessageDecorator<? extends Message>> container = new LdapMessageContainer(
+                ldapServer.getDirectoryService().getLdapCodecService() );
+            return ( BindResponse ) decoder.decode( _input_, container );
+        }
+
+
+        BindResponse bindType3( byte[] type3response ) throws Exception
+        {
+            if ( !isConnected() )
+            {
+                throw new IllegalStateException( "Client is not connected." );
+            }
+
+            // Setup the bind request
+            BindRequestImpl request = new BindRequestImpl();
+            request.setMessageId( 2 );
+            request.setDn( new Dn( "uid=admin,ou=system" ) );
+            request.setSimple( false );
+            request.setCredentials( type3response );
+            request.setSaslMechanism( mechanism );
+            request.setVersion3( true );
+
+            // Setup the ASN1 Enoder and Decoder
+            LdapDecoder decoder = new LdapDecoder();
+
+            // Send encoded request to server
+            LdapEncoder encoder = new LdapEncoder( getLdapServer().getDirectoryService().getLdapCodecService() );
+            ByteBuffer bb = encoder.encodeMessage( request );
+            bb.flip();
+
+            _output_.write( bb.array() );
+            _output_.flush();
+
+            while ( _input_.available() <= 0 )
+            {
+                Thread.sleep( 100 );
+            }
+
+            // Retrieve the response back from server to my last request.
+            LdapMessageContainer<MessageDecorator<? extends Message>> container = new LdapMessageContainer(
+                ldapServer.getDirectoryService().getLdapCodecService() );
+            return ( BindResponse ) decoder.decode( _input_, container );
+        }
+    }
+
+
+    private BogusNtlmProvider getNtlmProviderUsingReflection()
+    {
+        BogusNtlmProvider provider = null;
+        try
+        {
+            NtlmMechanismHandler ntlmHandler = ( NtlmMechanismHandler ) getLdapServer().getSaslMechanismHandlers().get(
+                SupportedSaslMechanisms.NTLM );
+
+            // there is no getter for 'provider' field hence this hack
+            Field field = ntlmHandler.getClass().getDeclaredField( "provider" );
+            field.setAccessible( true );
+            provider = ( BogusNtlmProvider ) field.get( ntlmHandler );
+        }
+        catch ( Exception e )
+        {
+            e.printStackTrace();
+        }
+
+        return provider;
+    }
+
+
+    ////////////////////////
+    protected Entry getPrincipalAttributes( String dn, String sn, String cn, String uid, String userPassword,
+        String principal ) throws LdapException
+    {
+        Entry entry = new DefaultEntry( dn );
+        entry.add( SchemaConstants.OBJECT_CLASS_AT, "person", "inetOrgPerson", "krb5principal", "krb5kdcentry" );
+        entry.add( SchemaConstants.CN_AT, cn );
+        entry.add( SchemaConstants.SN_AT, sn );
+        entry.add( SchemaConstants.UID_AT, uid );
+        entry.add( SchemaConstants.USER_PASSWORD_AT, userPassword );
+        entry.add( KerberosAttribute.KRB5_PRINCIPAL_NAME_AT, principal );
+        entry.add( KerberosAttribute.KRB5_KEY_VERSION_NUMBER_AT, "0" );
+
+        return entry;
+    }
+    */
+
+}