blob: a974d325ba0fdef7339886f3f704481079e1db3d [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.directory.server.operations.bind;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.reflect.Field;
import java.util.Hashtable;
import javax.naming.AuthenticationNotSupportedException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.net.SocketClient;
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.ldap.handlers.bind.cramMD5.CramMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.bind.digestMD5.DigestMd5MechanismHandler;
import org.apache.directory.server.ldap.handlers.bind.gssapi.GssapiMechanismHandler;
import org.apache.directory.server.ldap.handlers.bind.ntlm.NtlmMechanismHandler;
import org.apache.directory.server.ldap.handlers.bind.plain.PlainMechanismHandler;
import org.apache.directory.server.ldap.handlers.extended.StoredProcedureExtendedOperationHandler;
import org.apache.directory.shared.ldap.constants.SupportedSaslMechanisms;
import org.apache.directory.shared.ldap.message.BindRequestImpl;
import org.apache.directory.shared.ldap.message.MessageDecoder;
import org.apache.directory.shared.ldap.message.MessageEncoder;
import org.apache.directory.shared.ldap.message.ResultCodeEnum;
import org.apache.directory.shared.ldap.message.internal.InternalBindResponse;
import org.apache.directory.shared.ldap.message.spi.BinaryAttributeDetector;
import org.apache.directory.shared.ldap.name.DN;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An {@link AbstractServerTest} testing SASL authentication.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
* @version $Rev$, $Date$
*/
@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: top",
"uid: hnelson",
"userPassword: secret",
"cn: Horatio Nelson",
"sn: Nelson"
}
)
@CreateDS( allowAnonAccess=true, 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" )
} )
})
@CreateLdapServer (
transports =
{
@CreateTransport( protocol = "LDAP" )
},
saslHost="localhost",
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
)
public class SaslBindIT extends AbstractLdapTestUnit
{
/**
* Tests to make sure the server properly returns the supportedSASLMechanisms.
*/
@Test
public void testSupportedSASLMechanisms()
{
try
{
// We have to tell the server that it should accept anonymous
// auth, because we are reading the rootDSE
ldapServer.setAllowAnonymousAccess( true );
ldapServer.getDirectoryService().setAllowAnonymousAccess( true );
// Point on rootDSE
DirContext context = new InitialDirContext();
Attributes attrs = context.getAttributes( "ldap://localhost:"
+ ldapServer.getPort(), new String[]
{ "supportedSASLMechanisms" } );
// Thread.sleep( 10 * 60 * 1000 );
NamingEnumeration<? extends Attribute> answer = attrs.getAll();
Attribute result = answer.next();
assertEquals( 6, result.size() );
assertTrue( result.contains( SupportedSaslMechanisms.GSSAPI ) );
assertTrue( result.contains( SupportedSaslMechanisms.DIGEST_MD5 ) );
assertTrue( result.contains( SupportedSaslMechanisms.CRAM_MD5 ) );
assertTrue( result.contains( SupportedSaslMechanisms.NTLM ) );
assertTrue( result.contains( SupportedSaslMechanisms.PLAIN ) );
assertTrue( result.contains( SupportedSaslMechanisms.GSS_SPNEGO ) );
}
catch ( Exception e )
{
fail( "Should not have caught exception." );
}
}
/**
* Tests to make sure PLAIN-binds works
*/
@Test
public void testSaslBindPLAIN()
{
try
{
Hashtable<String, String> env = new Hashtable<String, String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapServer.getPort() );
env.put( Context.SECURITY_AUTHENTICATION, "PLAIN" );
env.put( Context.SECURITY_PRINCIPAL, "uid=hnelson,ou=users,dc=example,dc=com" );
env.put( Context.SECURITY_CREDENTIALS, "secret" );
DirContext context = new InitialDirContext( env );
String[] attrIDs =
{ "uid" };
Attributes attrs = context.getAttributes( "uid=hnelson,ou=users,dc=example,dc=com", attrIDs );
String uid = null;
if ( attrs.get( "uid" ) != null )
{
uid = ( String ) attrs.get( "uid" ).get();
}
assertEquals( uid, "hnelson" );
}
catch ( NamingException e )
{
fail( "Should not have caught exception." );
}
}
/**
* Test a SASL bind with an empty mechanism
*/
@Test
public void testSaslBindNoMech()
{
try
{
Hashtable<String, String> env = new Hashtable<String, String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapServer.getPort() );
env.put( Context.SECURITY_AUTHENTICATION, "" );
env.put( Context.SECURITY_PRINCIPAL, "uid=hnelson,ou=users,dc=example,dc=com" );
env.put( Context.SECURITY_CREDENTIALS, "secret" );
new InitialDirContext( env );
fail( "Should not be there" );
}
catch ( AuthenticationNotSupportedException anse )
{
assertTrue( true );
}
catch ( NamingException ne )
{
fail( "Should not have caught exception." );
}
}
/**
* Tests to make sure CRAM-MD5 binds below the RootDSE work.
*/
@Test
public void testSaslCramMd5Bind()
{
try
{
Hashtable<String, String> env = new Hashtable<String, String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapServer.getPort() );
env.put( Context.SECURITY_AUTHENTICATION, "CRAM-MD5" );
env.put( Context.SECURITY_PRINCIPAL, "hnelson" );
env.put( Context.SECURITY_CREDENTIALS, "secret" );
DirContext context = new InitialDirContext( env );
String[] attrIDs =
{ "uid" };
Attributes attrs = context.getAttributes( "uid=hnelson,ou=users,dc=example,dc=com", attrIDs );
String uid = null;
if ( attrs.get( "uid" ) != null )
{
uid = ( String ) attrs.get( "uid" ).get();
}
assertEquals( uid, "hnelson" );
}
catch ( NamingException e )
{
fail( "Should not have caught exception." );
}
}
/**
* Tests to make sure CRAM-MD5 binds below the RootDSE fail if the password is bad.
*/
@Test
public void testSaslCramMd5BindBadPassword()
{
try
{
Hashtable<String, String> env = new Hashtable<String, String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapServer.getPort() );
env.put( Context.SECURITY_AUTHENTICATION, "CRAM-MD5" );
env.put( Context.SECURITY_PRINCIPAL, "hnelson" );
env.put( Context.SECURITY_CREDENTIALS, "badsecret" );
DirContext context = new InitialDirContext( env );
String[] attrIDs =
{ "uid" };
context.getAttributes( "uid=hnelson,ou=users,dc=example,dc=com", attrIDs );
fail( "Should have thrown exception." );
}
catch ( NamingException e )
{
assertTrue( e.getMessage().contains( "Invalid response" ) );
}
}
/**
* Tests to make sure DIGEST-MD5 binds below the RootDSE work.
*/
@Test
public void testSaslDigestMd5Bind() throws Exception
{
Hashtable<String, String> env = new Hashtable<String, String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapServer.getPort() );
env.put( Context.SECURITY_AUTHENTICATION, "DIGEST-MD5" );
env.put( Context.SECURITY_PRINCIPAL, "hnelson" );
env.put( Context.SECURITY_CREDENTIALS, "secret" );
// Specify realm
env.put( "java.naming.security.sasl.realm", "example.com" );
// Request privacy protection
env.put( "javax.security.sasl.qop", "auth-conf" );
DirContext context = new InitialDirContext( env );
String[] attrIDs =
{ "uid" };
Attributes attrs = context.getAttributes( "uid=hnelson,ou=users,dc=example,dc=com", attrIDs );
String uid = null;
if ( attrs.get( "uid" ) != null )
{
uid = ( String ) attrs.get( "uid" ).get();
}
assertEquals( uid, "hnelson" );
}
/**
* Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the realm is bad.
*/
@Test
public void testSaslDigestMd5BindBadRealm()
{
try
{
Hashtable<String, String> env = new Hashtable<String, String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapServer.getPort() );
env.put( Context.SECURITY_AUTHENTICATION, "DIGEST-MD5" );
env.put( Context.SECURITY_PRINCIPAL, "hnelson" );
env.put( Context.SECURITY_CREDENTIALS, "secret" );
// Bad realm
env.put( "java.naming.security.sasl.realm", "badrealm.com" );
// Request privacy protection
env.put( "javax.security.sasl.qop", "auth-conf" );
DirContext context = new InitialDirContext( env );
String[] attrIDs =
{ "uid" };
context.getAttributes( "uid=hnelson,ou=users,dc=example,dc=com", attrIDs );
fail( "Should have thrown exception." );
}
catch ( NamingException e )
{
assertTrue( e.getMessage().contains( "Nonexistent realm" ) );
}
}
/**
* Tests to make sure DIGEST-MD5 binds below the RootDSE fail if the password is bad.
*/
@Test
public void testSaslDigestMd5BindBadPassword()
{
try
{
Hashtable<String, String> env = new Hashtable<String, String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, "ldap://localhost:" + ldapServer.getPort() );
env.put( Context.SECURITY_AUTHENTICATION, "DIGEST-MD5" );
env.put( Context.SECURITY_PRINCIPAL, "hnelson" );
env.put( Context.SECURITY_CREDENTIALS, "badsecret" );
DirContext context = new InitialDirContext( env );
String[] attrIDs = { "uid" };
context.getAttributes( "uid=hnelson,ou=users,dc=example,dc=com", attrIDs );
fail( "Should have thrown exception." );
}
catch ( NamingException e )
{
assertTrue( e.getMessage().contains( "digest response format violation" ) );
}
}
/**
* Tests that the plumbing for NTLM bind works.
*/
@Test
public void testNtlmBind() throws Exception
{
BogusNtlmProvider provider = getNtlmProviderUsingReflection();
NtlmSaslBindClient client = new NtlmSaslBindClient( SupportedSaslMechanisms.NTLM );
InternalBindResponse 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() ) );
InternalBindResponse 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 ) ldapServer.getSaslMechanismHandlers().get( SupportedSaslMechanisms.GSS_SPNEGO );
ntlmHandler.setNtlmProvider( provider );
NtlmSaslBindClient client = new NtlmSaslBindClient( SupportedSaslMechanisms.GSS_SPNEGO );
InternalBindResponse 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() ) );
InternalBindResponse finalResponse = client.bindType3( "type3_test".getBytes() );
assertEquals( 2, finalResponse.getMessageId() );
assertEquals( ResultCodeEnum.SUCCESS, finalResponse.getLdapResult().getResultCode() );
assertTrue( ArrayUtils.isEquals( "type3_test".getBytes(), provider.getType3Response() ) );
}
/**
* 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( ldapServer.getPort() );
connect( "localhost", ldapServer.getPort() );
setTcpNoDelay( false );
LOG.debug( "isConnected() = {}", isConnected() );
LOG.debug( "LocalPort = {}", getLocalPort() );
LOG.debug( "LocalAddress = {}", getLocalAddress() );
LOG.debug( "RemotePort = {}", getRemotePort() );
LOG.debug( "RemoteAddress = {}", getRemoteAddress() );
}
InternalBindResponse bindType1( byte[] type1response ) throws Exception
{
if ( ! isConnected() )
{
throw new IllegalStateException( "Client is not connected." );
}
// Setup the bind request
BindRequestImpl request = new BindRequestImpl( 1 ) ;
request.setName( new DN( "uid=admin,ou=system" ) ) ;
request.setSimple( false ) ;
request.setCredentials( type1response ) ;
request.setSaslMechanism( mechanism );
request.setVersion3( true ) ;
// Setup the ASN1 Encoder and Decoder
MessageEncoder encoder = new MessageEncoder();
MessageDecoder decoder = new MessageDecoder( new BinaryAttributeDetector() {
public boolean isBinary( String attributeId )
{
return false;
}
} );
// Send encoded request to server
encoder.encodeBlocking( null, _output_, request );
_output_.flush();
while ( _input_.available() <= 0 )
{
Thread.sleep( 100 );
}
// Retrieve the response back from server to my last request.
return ( InternalBindResponse ) decoder.decode( null, _input_ );
}
InternalBindResponse bindType3( byte[] type3response ) throws Exception
{
if ( ! isConnected() )
{
throw new IllegalStateException( "Client is not connected." );
}
// Setup the bind request
BindRequestImpl request = new BindRequestImpl( 2 ) ;
request.setName( new DN( "uid=admin,ou=system" ) ) ;
request.setSimple( false ) ;
request.setCredentials( type3response ) ;
request.setSaslMechanism( mechanism );
request.setVersion3( true ) ;
// Setup the ASN1 Enoder and Decoder
MessageEncoder encoder = new MessageEncoder();
MessageDecoder decoder = new MessageDecoder( new BinaryAttributeDetector() {
public boolean isBinary( String attributeId )
{
return false;
}
} );
// Send encoded request to server
encoder.encodeBlocking( null, _output_, request );
_output_.flush();
while ( _input_.available() <= 0 )
{
Thread.sleep( 100 );
}
// Retrieve the response back from server to my last request.
return ( InternalBindResponse ) decoder.decode( null, _input_ );
}
}
private BogusNtlmProvider getNtlmProviderUsingReflection()
{
BogusNtlmProvider provider = null;
try
{
NtlmMechanismHandler ntlmHandler = ( NtlmMechanismHandler ) ldapServer.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;
}
}