org.apache.directory.server.ldap.handlers.request.BindRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.directory.server.ldap.handlers.request.BindRequestHandler.java

Source

/*
 *  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.lang.exception.ExceptionUtils;
import org.apache.directory.api.ldap.codec.decorators.BindResponseDecorator;
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());
            } 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(new BindResponseDecorator(getLdapApiService(), bindResponse));

                return;
            }

            // 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(new BindResponseDecorator(getLdapApiService(), 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(new BindResponseDecorator(getLdapApiService(), 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(new BindResponseDecorator(getLdapApiService(), 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(new BindResponseDecorator(getLdapApiService(), 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(new BindResponseDecorator(getLdapApiService(), 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
     */
    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(new BindResponseDecorator(getLdapApiService(), bindResponse));

            return;
        }

        // Deal with the two kinds of authentication : Simple and SASL
        if (bindRequest.isSimple()) {
            handleSimpleAuth(ldapSession, bindRequest);
        } else {
            handleSaslAuth(ldapSession, bindRequest);
        }
    }
}