blob: 5e0f3fefc32c4d0fb3039420d77d89b146fe085e [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.ldap.handlers.request;
import java.util.Map;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
import org.apache.directory.api.ldap.model.message.BindRequest;
import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.LdapResult;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
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.entry.ClonedServerEntry;
import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
import org.apache.directory.server.core.shared.DefaultCoreSession;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.ldap.LdapProtocolUtils;
import org.apache.directory.server.ldap.LdapSession;
import org.apache.directory.server.ldap.handlers.LdapRequestHandler;
import org.apache.directory.server.ldap.handlers.sasl.MechanismHandler;
import org.apache.directory.server.ldap.handlers.sasl.SaslConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A single reply MessageReceived handler for {@link BindRequest}s.
*
* Implements server-side of RFC 2222, sections 4.2 and 4.3.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class BindRequestHandler extends LdapRequestHandler<BindRequest>
{
private static final Logger LOG = LoggerFactory.getLogger( BindRequestHandler.class );
/** A Hashed Adapter mapping SASL mechanisms to their handlers. */
private Map<String, MechanismHandler> handlers;
/**
* Set the mechanisms handler map.
*
* @param handlers The associations btween a machanism and its handler
*/
public void setSaslMechanismHandlers( Map<String, MechanismHandler> handlers )
{
this.handlers = handlers;
}
/**
* Handle the Simple authentication.
*
* @param ldapSession The associated Session
* @param bindRequest The BindRequest received
* @throws Exception If the authentication cannot be done
*/
// This will suppress PMD.EmptyCatchBlock warnings in this method
public void handleSimpleAuth( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
{
DirectoryService directoryService = ldapServer.getDirectoryService();
BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
// if the user is already bound, we have to unbind him
if ( ldapSession.isAuthenticated() )
{
// We already have a bound session for this user. We have to
// abandon it first.
ldapSession.getCoreSession().unbind();
}
// Set the status to SimpleAuthPending
ldapSession.setSimpleAuthPending();
// Now, bind the user
// create a new Bind context, with a null session, as we don't have
// any context yet.
BindOperationContext bindContext = new BindOperationContext( null );
// Stores the Dn of the user to check, and its password
Dn bindDn = bindRequest.getDn();
if ( bindDn == null )
{
String name = bindRequest.getName();
try
{
bindDn = new Dn( directoryService.getSchemaManager(), name );
bindRequest.setDn( bindDn );
}
catch ( LdapInvalidDnException e )
{
// This might still be a valid DN (Windows AD binding for instance)
LOG.debug( "Unable to convert the name to a DN." );
}
}
bindContext.setDn( bindRequest.getDn() );
bindContext.setCredentials( bindRequest.getCredentials() );
bindContext.setIoSession( ldapSession.getIoSession() );
bindContext.setInterceptors( directoryService.getInterceptors( OperationEnum.BIND ) );
// Stores the request controls into the operation context
LdapProtocolUtils.setRequestControls( bindContext, bindRequest );
try
{
/*
* Referral handling as specified by RFC 3296 here:
*
* http://www.faqs.org/rfcs/rfc3296.html
*
* See section 5.6.1 where if the bind principal Dn is a referral
* we return an invalidCredentials result response. Optionally we
* could support delegated authentication in the future with this
* potential. See the following JIRA for more on this possibility:
*
* https://issues.apache.org/jira/browse/DIRSERVER-1217
*
* NOTE: if this is done then this handler should extend the
* a modified form of the ReferralAwareRequestHandler so it can
* detect conditions where ancestors of the Dn are referrals
* and delegate appropriately.
*/
Entry principalEntry = null;
try
{
principalEntry = directoryService.getAdminSession().lookup( bindRequest.getDn(),
SchemaConstants.ALL_USER_ATTRIBUTES, SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES );
}
catch ( Exception le )
{
// this is OK
}
if ( principalEntry == null )
{
LOG.info( "The {} principalDN cannot be found in the server : bind failure.", bindRequest.getName() );
}
else if ( ( ( ClonedServerEntry ) principalEntry ).getOriginalEntry().contains(
SchemaConstants.OBJECT_CLASS_AT,
SchemaConstants.REFERRAL_OC ) )
{
LOG.info( "Bind principalDn points to referral." );
LdapResult result = bindResponse.getLdapResult();
result.setDiagnosticMessage( "Bind principalDn points to referral." );
result.setResultCode( ResultCodeEnum.INVALID_CREDENTIALS );
// Reset the session now
ldapSession.setAnonymous();
// Write the response
ldapSession.getIoSession().write( bindResponse );
return;
}
else
{
bindContext.setPrincipal( principalEntry );
}
// TODO - might cause issues since lookups are not returning all
// attributes right now - this is an optimization that can be
// enabled later after determining whether or not this will cause
// issues.
// reuse the looked up entry so we don't incur another lookup
// opContext.setEntry( principalEntry );
// And call the OperationManager bind operation.
bindContext.setInterceptors( directoryService.getInterceptors( OperationEnum.BIND ) );
directoryService.getOperationManager().bind( bindContext );
// As a result, store the created session in the Core Session
CoreSession coreSession = bindContext.getSession();
ldapSession.setCoreSession( coreSession );
// Store the IoSession in the coreSession
( ( DefaultCoreSession ) coreSession ).setIoSession( bindContext.getIoSession() );
// And set the current state accordingly
if ( !ldapSession.getCoreSession().isAnonymous() )
{
ldapSession.setAuthenticated();
}
else
{
ldapSession.setAnonymous();
}
// Return the successful response
bindResponse.addAllControls( bindContext.getResponseControls() );
sendBindSuccess( ldapSession, bindResponse, null );
}
catch ( Exception e )
{
// Something went wrong. Write back an error message
// For BindRequest, it should be an InvalidCredentials,
// no matter what kind of exception we got.
ResultCodeEnum code = null;
LdapResult result = bindResponse.getLdapResult();
if ( e instanceof LdapUnwillingToPerformException )
{
code = ResultCodeEnum.UNWILLING_TO_PERFORM;
result.setResultCode( code );
}
else if ( e instanceof LdapInvalidDnException )
{
code = ResultCodeEnum.INVALID_DN_SYNTAX;
result.setResultCode( code );
}
else
{
code = ResultCodeEnum.INVALID_CREDENTIALS;
result.setResultCode( code );
}
String msg = code.toString() + ": Bind failed: " + e.getLocalizedMessage();
if ( LOG.isDebugEnabled() )
{
msg += ":\n" + ExceptionUtils.getStackTrace( e );
msg += "\n\nBindRequest = \n" + bindRequest.toString();
}
Dn dn = null;
if ( e instanceof LdapAuthenticationException )
{
dn = ( ( LdapAuthenticationException ) e ).getResolvedDn();
}
if ( ( dn != null )
&& ( ( code == ResultCodeEnum.NO_SUCH_OBJECT ) || ( code == ResultCodeEnum.ALIAS_PROBLEM )
|| ( code == ResultCodeEnum.INVALID_DN_SYNTAX ) || ( code == ResultCodeEnum.ALIAS_DEREFERENCING_PROBLEM ) ) )
{
result.setMatchedDn( dn );
}
result.setDiagnosticMessage( msg );
bindResponse.addAllControls( bindContext.getResponseControls() );
// Before writing the response, be sure the session is set to anonymous
ldapSession.setAnonymous();
// Write the response
ldapSession.getIoSession().write( bindResponse );
}
finally
{
// Reset LDAP session bind status to anonymous if authentication failed
if ( !ldapSession.isAuthenticated() )
{
ldapSession.setAnonymous();
}
}
}
/**
* Check if the mechanism exists.
*/
private boolean checkMechanism( String saslMechanism )
{
// Guard clause: Reject unsupported SASL mechanisms.
if ( !ldapServer.getSupportedMechanisms().contains( saslMechanism ) )
{
LOG.error( I18n.err( I18n.ERR_160, saslMechanism ) );
return false;
}
else
{
return true;
}
}
/**
* For challenge/response exchange, generate the challenge.
* If the exchange is complete then send bind success.
*
* @param ldapSession
* @param ss
* @param bindRequest
*/
private void generateSaslChallengeOrComplete( LdapSession ldapSession, SaslServer ss,
BindRequest bindRequest ) throws Exception
{
BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
LdapResult result = bindResponse.getLdapResult();
// SaslServer will throw an exception if the credentials are null.
if ( bindRequest.getCredentials() == null )
{
bindRequest.setCredentials( Strings.EMPTY_BYTES );
}
try
{
// Compute the challenge
byte[] tokenBytes = ss.evaluateResponse( bindRequest.getCredentials() );
if ( ss.isComplete() )
{
// This is the end of the C/R exchange
if ( tokenBytes != null )
{
/*
* There may be a token to return to the client. We set it here
* so it will be returned in a SUCCESS message, after an LdapContext
* has been initialized for the client.
*/
ldapSession.putSaslProperty( SaslConstants.SASL_CREDS, tokenBytes );
}
LdapPrincipal ldapPrincipal = ( LdapPrincipal ) ldapSession
.getSaslProperty( SaslConstants.SASL_AUTHENT_USER );
if ( ldapPrincipal != null )
{
DirectoryService ds = ldapSession.getLdapServer().getDirectoryService();
String saslMechanism = bindRequest.getSaslMechanism();
byte[] password = null;
if ( ldapPrincipal.getUserPasswords() != null )
{
password = ldapPrincipal.getUserPasswords()[0];
}
CoreSession userSession = ds.getSession( ldapPrincipal.getDn(),
password, saslMechanism, null );
// Set the user session into the ldap session
ldapSession.setCoreSession( userSession );
// Store the IoSession in the coreSession
( ( DefaultCoreSession ) userSession ).setIoSession( ldapSession.getIoSession() );
}
// Mark the user as authenticated
ldapSession.setAuthenticated();
// Call the cleanup method for the selected mechanism
MechanismHandler handler = ( MechanismHandler ) ldapSession
.getSaslProperty( SaslConstants.SASL_MECH_HANDLER );
handler.cleanup( ldapSession );
// Return the successful response
sendBindSuccess( ldapSession, bindResponse, tokenBytes );
}
else
{
// The SASL bind must continue, we are sending the computed challenge
LOG.info( "Continuation token had length {}", tokenBytes.length );
// Build the response
result.setResultCode( ResultCodeEnum.SASL_BIND_IN_PROGRESS );
// Store the challenge
bindResponse.setServerSaslCreds( tokenBytes );
// Switch to SASLAuthPending
ldapSession.setSaslAuthPending();
// And write back the response
ldapSession.getIoSession().write( bindResponse );
LOG.debug( "Returning final authentication data to client to complete context." );
}
}
catch ( SaslException se )
{
sendInvalidCredentials( ldapSession, bindResponse, se );
}
}
/**
* Send back an AUTH-METH-NOT-SUPPORTED error message to the client
*/
private void sendAuthMethNotSupported( LdapSession ldapSession, BindRequest bindRequest )
{
BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
// First, re-init the state to Anonymous, and clear the
// saslProperty map
ldapSession.clearSaslProperties();
ldapSession.setAnonymous();
// And send the response to the client
LdapResult bindResult = bindResponse.getLdapResult();
bindResult.setResultCode( ResultCodeEnum.AUTH_METHOD_NOT_SUPPORTED );
bindResult.setDiagnosticMessage( ResultCodeEnum.AUTH_METHOD_NOT_SUPPORTED.toString() + ": "
+ bindRequest.getSaslMechanism() + " is not a supported mechanism." );
// Write back the error
ldapSession.getIoSession().write( bindResponse );
}
/**
* Send back an INVALID-CREDENTIAL error message to the user. If we have an exception
* as a third argument, then send back the associated message to the client.
*/
private void sendInvalidCredentials( LdapSession ldapSession, BindResponse bindResponse, Exception e )
{
LdapResult result = bindResponse.getLdapResult();
String message = "";
if ( e != null )
{
message = ResultCodeEnum.INVALID_CREDENTIALS + ": " + e.getLocalizedMessage();
}
else
{
message = ResultCodeEnum.INVALID_CREDENTIALS.toString();
}
LOG.error( message );
result.setResultCode( ResultCodeEnum.INVALID_CREDENTIALS );
result.setDiagnosticMessage( message );
// Reinitialize the state to Anonymous and clear the sasl properties
ldapSession.clearSaslProperties();
ldapSession.setAnonymous();
// Write back the error response
ldapSession.getIoSession().write( bindResponse );
}
/**
* Send a SUCCESS message back to the client.
*/
private void sendBindSuccess( LdapSession ldapSession, BindResponse bindResponse, byte[] tokenBytes )
{
// Return the successful response
bindResponse.getLdapResult().setResultCode( ResultCodeEnum.SUCCESS );
bindResponse.setServerSaslCreds( tokenBytes );
if ( !ldapSession.getCoreSession().isAnonymous() )
{
// If we have not been asked to authenticate as Anonymous, authenticate the user
ldapSession.setAuthenticated();
}
else
{
// Otherwise, switch back to Anonymous
ldapSession.setAnonymous();
}
// Clean the SaslProperties, we don't need them anymore
MechanismHandler handler = ( MechanismHandler ) ldapSession.getSaslProperty( SaslConstants.SASL_MECH_HANDLER );
if ( handler != null )
{
handler.cleanup( ldapSession );
}
ldapSession.getIoSession().write( bindResponse );
LOG.debug( "Returned SUCCESS message: {}.", bindResponse );
}
private void handleSaslAuthPending( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
{
// First, check that we have the same mechanism
String saslMechanism = bindRequest.getSaslMechanism();
// The empty mechanism is also a request for a new Bind session
if ( Strings.isEmpty( saslMechanism )
|| !ldapSession.getSaslProperty( SaslConstants.SASL_MECH ).equals( saslMechanism ) )
{
sendAuthMethNotSupported( ldapSession, bindRequest );
return;
}
// We have already received a first BindRequest, and sent back some challenge.
// First, check if the mechanism is the same
MechanismHandler mechanismHandler = handlers.get( saslMechanism );
if ( mechanismHandler == null )
{
String message = I18n.err( I18n.ERR_161, saslMechanism );
// Clear the saslProperties, and move to the anonymous state
ldapSession.clearSaslProperties();
ldapSession.setAnonymous();
LOG.error( message );
throw new IllegalArgumentException( message );
}
// Get the previously created SaslServer instance
SaslServer ss = mechanismHandler.handleMechanism( ldapSession, bindRequest );
generateSaslChallengeOrComplete( ldapSession, ss, bindRequest );
}
/**
* Handle the SASL authentication. If the mechanism is known, we are
* facing three cases :
* <ul>
* <li>The user does not has a session yet</li>
* <li>The user already has a session</li>
* <li>The user has started a SASL negotiation</li>
* </ul>
*
* In the first case, we initiate a SaslBind session, which will be used all
* along the negotiation.<br>
* In the second case, we first have to unbind the user, and initiate a new
* SaslBind session.<br>
* In the third case, we have sub cases :
* <ul>
* <li>The mechanism is not provided : that means the user want to reset the
* current negotiation. We move back to an Anonymous state</li>
* <li>The mechanism is provided : the user is initializing a new negotiation
* with another mechanism. The current SaslBind session is reinitialized</li>
* </ul><br>
*
* @param ldapSession The associated Session
* @param bindRequest The BindRequest received
* @throws Exception If the authentication cannot be done
*/
public void handleSaslAuth( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
{
String saslMechanism = bindRequest.getSaslMechanism();
// Case #2 : the user does have a session. We have to unbind him
if ( ldapSession.isAuthenticated() )
{
// We already have a bound session for this user. We have to
// close the previous session first.
ldapSession.getCoreSession().unbind();
// Reset the status to Anonymous
ldapSession.setAnonymous();
// Clean the sasl properties
ldapSession.clearSaslProperties();
// Now we can continue as if the client was Anonymous from the beginning
}
// case #1 : The user does not have a session.
if ( ldapSession.isAnonymous() )
{
// fist check that the mechanism exists
if ( !checkMechanism( saslMechanism ) )
{
// get out !
sendAuthMethNotSupported( ldapSession, bindRequest );
return;
}
// Store the mechanism in the ldap session
ldapSession.putSaslProperty( SaslConstants.SASL_MECH, saslMechanism );
// Get the handler for this mechanism
MechanismHandler mechanismHandler = handlers.get( saslMechanism );
// Store the mechanism handler in the salsProperties
ldapSession.putSaslProperty( SaslConstants.SASL_MECH_HANDLER, mechanismHandler );
// Initialize the mechanism specific data
mechanismHandler.init( ldapSession );
// Get the SaslServer instance which manage the C/R exchange
SaslServer ss = mechanismHandler.handleMechanism( ldapSession, bindRequest );
// We have to generate a challenge
generateSaslChallengeOrComplete( ldapSession, ss, bindRequest );
// And get back
}
else if ( ldapSession.isAuthPending() )
{
try
{
handleSaslAuthPending( ldapSession, bindRequest );
}
catch ( SaslException se )
{
sendInvalidCredentials( ldapSession, ( BindResponse ) bindRequest.getResultResponse(), se );
}
}
}
/**
* Deal with a received BindRequest
*
* @param ldapSession The current session
* @param bindRequest The received BindRequest
* @throws Exception If the authentication cannot be handled
*/
@Override
public void handle( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
{
LOG.debug( "Received: {}", bindRequest );
// Guard clause: LDAP version 3
if ( !bindRequest.getVersion3() )
{
BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
LOG.error( I18n.err( I18n.ERR_162 ) );
LdapResult bindResult = bindResponse.getLdapResult();
bindResult.setResultCode( ResultCodeEnum.PROTOCOL_ERROR );
bindResult.setDiagnosticMessage( I18n.err( I18n.ERR_163 ) );
ldapSession.getIoSession().write( bindResponse );
return;
}
// Deal with the two kinds of authentication : Simple and SASL
if ( bindRequest.isSimple() )
{
handleSimpleAuth( ldapSession, bindRequest );
}
else
{
handleSaslAuth( ldapSession, bindRequest );
}
}
}