001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.server.ldap.handlers.request;
021
022
023import java.util.Map;
024
025import javax.security.sasl.SaslException;
026import javax.security.sasl.SaslServer;
027
028import org.apache.commons.lang3.exception.ExceptionUtils;
029import org.apache.directory.api.ldap.model.constants.SchemaConstants;
030import org.apache.directory.api.ldap.model.entry.Entry;
031import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
032import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
033import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
034import org.apache.directory.api.ldap.model.message.BindRequest;
035import org.apache.directory.api.ldap.model.message.BindResponse;
036import org.apache.directory.api.ldap.model.message.LdapResult;
037import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
038import org.apache.directory.api.ldap.model.name.Dn;
039import org.apache.directory.api.util.Strings;
040import org.apache.directory.server.core.api.CoreSession;
041import org.apache.directory.server.core.api.DirectoryService;
042import org.apache.directory.server.core.api.LdapPrincipal;
043import org.apache.directory.server.core.api.OperationEnum;
044import org.apache.directory.server.core.api.entry.ClonedServerEntry;
045import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
046import org.apache.directory.server.core.shared.DefaultCoreSession;
047import org.apache.directory.server.i18n.I18n;
048import org.apache.directory.server.ldap.LdapProtocolUtils;
049import org.apache.directory.server.ldap.LdapSession;
050import org.apache.directory.server.ldap.handlers.LdapRequestHandler;
051import org.apache.directory.server.ldap.handlers.sasl.MechanismHandler;
052import org.apache.directory.server.ldap.handlers.sasl.SaslConstants;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056
057/**
058 * A single reply MessageReceived handler for {@link BindRequest}s.
059 *
060 * Implements server-side of RFC 2222, sections 4.2 and 4.3.
061 *
062 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
063 */
064public class BindRequestHandler extends LdapRequestHandler<BindRequest>
065{
066    private static final Logger LOG = LoggerFactory.getLogger( BindRequestHandler.class );
067
068    /** A Hashed Adapter mapping SASL mechanisms to their handlers. */
069    private Map<String, MechanismHandler> handlers;
070
071
072    /**
073     * Set the mechanisms handler map.
074     *
075     * @param handlers The associations btween a machanism and its handler
076     */
077    public void setSaslMechanismHandlers( Map<String, MechanismHandler> handlers )
078    {
079        this.handlers = handlers;
080    }
081
082
083    /**
084     * Handle the Simple authentication.
085     *
086     * @param ldapSession The associated Session
087     * @param bindRequest The BindRequest received
088     * @throws Exception If the authentication cannot be done
089     */
090    // This will suppress PMD.EmptyCatchBlock warnings in this method
091    public void handleSimpleAuth( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
092    {
093        DirectoryService directoryService = ldapServer.getDirectoryService();
094        BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
095
096        // if the user is already bound, we have to unbind him
097        if ( ldapSession.isAuthenticated() )
098        {
099            // We already have a bound session for this user. We have to
100            // abandon it first.
101            ldapSession.getCoreSession().unbind();
102        }
103
104        // Set the status to SimpleAuthPending
105        ldapSession.setSimpleAuthPending();
106
107        // Now, bind the user
108
109        // create a new Bind context, with a null session, as we don't have
110        // any context yet.
111        BindOperationContext bindContext = new BindOperationContext( null );
112
113        // Stores the Dn of the user to check, and its password
114        Dn bindDn = bindRequest.getDn();
115
116        if ( bindDn == null )
117        {
118            String name = bindRequest.getName();
119
120            try
121            {
122                bindDn = new Dn( directoryService.getSchemaManager(), name );
123                bindRequest.setDn( bindDn );
124            }
125            catch ( LdapInvalidDnException e )
126            {
127                // This might still be a valid DN (Windows AD binding for instance)
128                LOG.debug( "Unable to convert the name to a DN." );
129            }
130        }
131
132        bindContext.setDn( bindRequest.getDn() );
133        bindContext.setCredentials( bindRequest.getCredentials() );
134        bindContext.setIoSession( ldapSession.getIoSession() );
135        bindContext.setInterceptors( directoryService.getInterceptors( OperationEnum.BIND ) );
136
137        // Stores the request controls into the operation context
138        LdapProtocolUtils.setRequestControls( bindContext, bindRequest );
139
140        try
141        {
142            /*
143             * Referral handling as specified by RFC 3296 here:
144             *
145             *      http://www.faqs.org/rfcs/rfc3296.html
146             *
147             * See section 5.6.1 where if the bind principal Dn is a referral
148             * we return an invalidCredentials result response.  Optionally we
149             * could support delegated authentication in the future with this
150             * potential.  See the following JIRA for more on this possibility:
151             *
152             *      https://issues.apache.org/jira/browse/DIRSERVER-1217
153             *
154             * NOTE: if this is done then this handler should extend the
155             * a modified form of the ReferralAwareRequestHandler so it can
156             * detect conditions where ancestors of the Dn are referrals
157             * and delegate appropriately.
158             */
159            Entry principalEntry = null;
160
161            try
162            {
163                principalEntry = directoryService.getAdminSession().lookup( bindRequest.getDn(), 
164                    SchemaConstants.ALL_USER_ATTRIBUTES, SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES );
165            }
166            catch ( Exception le )
167            {
168                // this is OK, it may be a delegated authentication, and in this case
169                // the entry is not present locally
170            }
171
172            if ( principalEntry == null )
173            {
174                LOG.info( "The {} principalDN cannot be found in the server : bind failure.", bindRequest.getName() );
175            }
176            else if ( ( ( ClonedServerEntry ) principalEntry ).getOriginalEntry().contains(
177                SchemaConstants.OBJECT_CLASS_AT,
178                SchemaConstants.REFERRAL_OC ) )
179            {
180                LOG.info( "Bind principalDn points to referral." );
181                LdapResult result = bindResponse.getLdapResult();
182                result.setDiagnosticMessage( "Bind principalDn points to referral." );
183                result.setResultCode( ResultCodeEnum.INVALID_CREDENTIALS );
184
185                // Reset the session now
186                ldapSession.setAnonymous();
187
188                // Write the response
189                ldapSession.getIoSession().write( bindResponse );
190
191                return;
192            }
193            else
194            { 
195                bindContext.setPrincipal( principalEntry );
196            }
197
198            // TODO - might cause issues since lookups are not returning all
199            // attributes right now - this is an optimization that can be
200            // enabled later after determining whether or not this will cause
201            // issues.
202            // reuse the looked up entry so we don't incur another lookup
203            // opContext.setEntry( principalEntry );
204
205            // And call the OperationManager bind operation.
206            bindContext.setInterceptors( directoryService.getInterceptors( OperationEnum.BIND ) );
207            directoryService.getOperationManager().bind( bindContext );
208
209            // As a result, store the created session in the Core Session
210            CoreSession coreSession = bindContext.getSession();
211            ldapSession.setCoreSession( coreSession );
212
213            // Store the IoSession in the coreSession
214            ( ( DefaultCoreSession ) coreSession ).setIoSession( bindContext.getIoSession() );
215
216            // And set the current state accordingly
217            if ( !ldapSession.getCoreSession().isAnonymous() )
218            {
219                ldapSession.setAuthenticated();
220            }
221            else
222            {
223                ldapSession.setAnonymous();
224            }
225
226            // Return the successful response
227            bindResponse.addAllControls( bindContext.getResponseControls() );
228            sendBindSuccess( ldapSession, bindResponse, null );
229        }
230        catch ( Exception e )
231        {
232            // Something went wrong. Write back an error message
233            // For BindRequest, it should be an InvalidCredentials,
234            // no matter what kind of exception we got.
235            ResultCodeEnum code = null;
236            LdapResult result = bindResponse.getLdapResult();
237
238            if ( e instanceof LdapUnwillingToPerformException )
239            {
240                code = ResultCodeEnum.UNWILLING_TO_PERFORM;
241                result.setResultCode( code );
242            }
243            else if ( e instanceof LdapInvalidDnException )
244            {
245                code = ResultCodeEnum.INVALID_DN_SYNTAX;
246                result.setResultCode( code );
247            }
248            else
249            {
250                code = ResultCodeEnum.INVALID_CREDENTIALS;
251                result.setResultCode( code );
252            }
253
254            String msg = code.toString() + ": Bind failed: " + e.getLocalizedMessage();
255
256            if ( LOG.isDebugEnabled() )
257            {
258                msg += ":\n" + ExceptionUtils.getStackTrace( e );
259                msg += "\n\nBindRequest = \n" + bindRequest.toString();
260            }
261
262            Dn dn = null;
263
264            if ( e instanceof LdapAuthenticationException )
265            {
266                dn = ( ( LdapAuthenticationException ) e ).getResolvedDn();
267            }
268
269            if ( ( dn != null )
270                && ( ( code == ResultCodeEnum.NO_SUCH_OBJECT ) || ( code == ResultCodeEnum.ALIAS_PROBLEM )
271                    || ( code == ResultCodeEnum.INVALID_DN_SYNTAX ) || ( code == ResultCodeEnum.ALIAS_DEREFERENCING_PROBLEM ) ) )
272            {
273                result.setMatchedDn( dn );
274            }
275
276            result.setDiagnosticMessage( msg );
277            bindResponse.addAllControls( bindContext.getResponseControls() );
278
279            // Before writing the response, be sure the session is set to anonymous
280            ldapSession.setAnonymous();
281
282            // Write the response
283            ldapSession.getIoSession().write( bindResponse );
284        }
285        finally
286        {
287            // Reset LDAP session bind status to anonymous if authentication failed
288            if ( !ldapSession.isAuthenticated() )
289            {
290                ldapSession.setAnonymous();
291            }
292        }
293    }
294
295
296    /**
297     * Check if the mechanism exists.
298     */
299    private boolean checkMechanism( String saslMechanism )
300    {
301        // Guard clause:  Reject unsupported SASL mechanisms.
302        if ( !ldapServer.getSupportedMechanisms().contains( saslMechanism ) )
303        {
304            LOG.error( I18n.err( I18n.ERR_160, saslMechanism ) );
305
306            return false;
307        }
308        else
309        {
310            return true;
311        }
312    }
313
314
315    /**
316     * For challenge/response exchange, generate the challenge.
317     * If the exchange is complete then send bind success.
318     *
319     * @param ldapSession
320     * @param ss
321     * @param bindRequest
322     */
323    private void generateSaslChallengeOrComplete( LdapSession ldapSession, SaslServer ss,
324        BindRequest bindRequest ) throws Exception
325    {
326        BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
327
328        LdapResult result = bindResponse.getLdapResult();
329
330        // SaslServer will throw an exception if the credentials are null.
331        if ( bindRequest.getCredentials() == null )
332        {
333            bindRequest.setCredentials( Strings.EMPTY_BYTES );
334        }
335
336        try
337        {
338            // Compute the challenge
339            byte[] tokenBytes = ss.evaluateResponse( bindRequest.getCredentials() );
340
341            if ( ss.isComplete() )
342            {
343                // This is the end of the C/R exchange
344                if ( tokenBytes != null )
345                {
346                    /*
347                     * There may be a token to return to the client.  We set it here
348                     * so it will be returned in a SUCCESS message, after an LdapContext
349                     * has been initialized for the client.
350                     */
351                    ldapSession.putSaslProperty( SaslConstants.SASL_CREDS, tokenBytes );
352                }
353
354                LdapPrincipal ldapPrincipal = ( LdapPrincipal ) ldapSession
355                    .getSaslProperty( SaslConstants.SASL_AUTHENT_USER );
356
357                if ( ldapPrincipal != null )
358                {
359                    DirectoryService ds = ldapSession.getLdapServer().getDirectoryService();
360                    String saslMechanism = bindRequest.getSaslMechanism();
361                    byte[] password = null;
362
363                    if ( ldapPrincipal.getUserPasswords() != null )
364                    {
365                        password = ldapPrincipal.getUserPasswords()[0];
366                    }
367
368                    CoreSession userSession = ds.getSession( ldapPrincipal.getDn(),
369                        password, saslMechanism, null );
370
371                    // Set the user session into the ldap session
372                    ldapSession.setCoreSession( userSession );
373
374                    // Store the IoSession in the coreSession
375                    ( ( DefaultCoreSession ) userSession ).setIoSession( ldapSession.getIoSession() );
376                }
377
378                // Mark the user as authenticated
379                ldapSession.setAuthenticated();
380
381                // Call the cleanup method for the selected mechanism
382                MechanismHandler handler = ( MechanismHandler ) ldapSession
383                    .getSaslProperty( SaslConstants.SASL_MECH_HANDLER );
384                handler.cleanup( ldapSession );
385
386                // Return the successful response
387                sendBindSuccess( ldapSession, bindResponse, tokenBytes );
388            }
389            else
390            {
391                // The SASL bind must continue, we are sending the computed challenge
392                LOG.info( "Continuation token had length {}", tokenBytes.length );
393
394                // Build the response
395                result.setResultCode( ResultCodeEnum.SASL_BIND_IN_PROGRESS );
396
397                // Store the challenge
398                bindResponse.setServerSaslCreds( tokenBytes );
399
400                // Switch to SASLAuthPending
401                ldapSession.setSaslAuthPending();
402
403                // And write back the response
404                ldapSession.getIoSession().write( bindResponse );
405
406                LOG.debug( "Returning final authentication data to client to complete context." );
407            }
408        }
409        catch ( SaslException se )
410        {
411            sendInvalidCredentials( ldapSession, bindResponse, se );
412        }
413    }
414
415
416    /**
417     * Send back an AUTH-METH-NOT-SUPPORTED error message to the client
418     */
419    private void sendAuthMethNotSupported( LdapSession ldapSession, BindRequest bindRequest )
420    {
421        BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
422
423        // First, re-init the state to Anonymous, and clear the
424        // saslProperty map
425        ldapSession.clearSaslProperties();
426        ldapSession.setAnonymous();
427
428        // And send the response to the client
429        LdapResult bindResult = bindResponse.getLdapResult();
430        bindResult.setResultCode( ResultCodeEnum.AUTH_METHOD_NOT_SUPPORTED );
431        bindResult.setDiagnosticMessage( ResultCodeEnum.AUTH_METHOD_NOT_SUPPORTED.toString() + ": "
432            + bindRequest.getSaslMechanism() + " is not a supported mechanism." );
433
434        // Write back the error
435        ldapSession.getIoSession().write( bindResponse );
436    }
437
438
439    /**
440     * Send back an INVALID-CREDENTIAL error message to the user. If we have an exception
441     * as a third argument, then send back the associated message to the client.
442     */
443    private void sendInvalidCredentials( LdapSession ldapSession, BindResponse bindResponse, Exception e )
444    {
445        LdapResult result = bindResponse.getLdapResult();
446
447        String message = "";
448
449        if ( e != null )
450        {
451            message = ResultCodeEnum.INVALID_CREDENTIALS + ": " + e.getLocalizedMessage();
452        }
453        else
454        {
455            message = ResultCodeEnum.INVALID_CREDENTIALS.toString();
456        }
457
458        LOG.error( message );
459        result.setResultCode( ResultCodeEnum.INVALID_CREDENTIALS );
460        result.setDiagnosticMessage( message );
461
462        // Reinitialize the state to Anonymous and clear the sasl properties
463        ldapSession.clearSaslProperties();
464        ldapSession.setAnonymous();
465
466        // Write back the error response
467        ldapSession.getIoSession().write( bindResponse );
468    }
469
470
471    /**
472     * Send a SUCCESS message back to the client.
473     */
474    private void sendBindSuccess( LdapSession ldapSession, BindResponse bindResponse, byte[] tokenBytes )
475    {
476        // Return the successful response
477        bindResponse.getLdapResult().setResultCode( ResultCodeEnum.SUCCESS );
478        bindResponse.setServerSaslCreds( tokenBytes );
479
480        if ( !ldapSession.getCoreSession().isAnonymous() )
481        {
482            // If we have not been asked to authenticate as Anonymous, authenticate the user
483            ldapSession.setAuthenticated();
484        }
485        else
486        {
487            // Otherwise, switch back to Anonymous
488            ldapSession.setAnonymous();
489        }
490
491        // Clean the SaslProperties, we don't need them anymore
492        MechanismHandler handler = ( MechanismHandler ) ldapSession.getSaslProperty( SaslConstants.SASL_MECH_HANDLER );
493
494        if ( handler != null )
495        {
496            handler.cleanup( ldapSession );
497        }
498
499        ldapSession.getIoSession().write( bindResponse );
500
501        LOG.debug( "Returned SUCCESS message: {}.", bindResponse );
502    }
503
504
505    private void handleSaslAuthPending( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
506    {
507        // First, check that we have the same mechanism
508        String saslMechanism = bindRequest.getSaslMechanism();
509
510        // The empty mechanism is also a request for a new Bind session
511        if ( Strings.isEmpty( saslMechanism )
512            || !ldapSession.getSaslProperty( SaslConstants.SASL_MECH ).equals( saslMechanism ) )
513        {
514            sendAuthMethNotSupported( ldapSession, bindRequest );
515            return;
516        }
517
518        // We have already received a first BindRequest, and sent back some challenge.
519        // First, check if the mechanism is the same
520        MechanismHandler mechanismHandler = handlers.get( saslMechanism );
521
522        if ( mechanismHandler == null )
523        {
524            String message = I18n.err( I18n.ERR_161, saslMechanism );
525
526            // Clear the saslProperties, and move to the anonymous state
527            ldapSession.clearSaslProperties();
528            ldapSession.setAnonymous();
529
530            LOG.error( message );
531            throw new IllegalArgumentException( message );
532        }
533
534        // Get the previously created SaslServer instance
535        SaslServer ss = mechanismHandler.handleMechanism( ldapSession, bindRequest );
536
537        generateSaslChallengeOrComplete( ldapSession, ss, bindRequest );
538    }
539
540
541    /**
542     * Handle the SASL authentication. If the mechanism is known, we are
543     * facing three cases :
544     * <ul>
545     * <li>The user does not has a session yet</li>
546     * <li>The user already has a session</li>
547     * <li>The user has started a SASL negotiation</li>
548     * </ul>
549     *
550     * In the first case, we initiate a SaslBind session, which will be used all
551     * along the negotiation.<br>
552     * In the second case, we first have to unbind the user, and initiate a new
553     * SaslBind session.<br>
554     * In the third case, we have sub cases :
555     * <ul>
556     * <li>The mechanism is not provided : that means the user want to reset the
557     * current negotiation. We move back to an Anonymous state</li>
558     * <li>The mechanism is provided : the user is initializing a new negotiation
559     * with another mechanism. The current SaslBind session is reinitialized</li>
560     * </ul><br>
561     *
562     * @param ldapSession The associated Session
563     * @param bindRequest The BindRequest received
564     * @throws Exception If the authentication cannot be done
565     */
566    public void handleSaslAuth( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
567    {
568        String saslMechanism = bindRequest.getSaslMechanism();
569
570        // Case #2 : the user does have a session. We have to unbind him
571        if ( ldapSession.isAuthenticated() )
572        {
573            // We already have a bound session for this user. We have to
574            // close the previous session first.
575            ldapSession.getCoreSession().unbind();
576
577            // Reset the status to Anonymous
578            ldapSession.setAnonymous();
579
580            // Clean the sasl properties
581            ldapSession.clearSaslProperties();
582
583            // Now we can continue as if the client was Anonymous from the beginning
584        }
585
586        // case #1 : The user does not have a session.
587        if ( ldapSession.isAnonymous() )
588        {
589            // fist check that the mechanism exists
590            if ( !checkMechanism( saslMechanism ) )
591            {
592                // get out !
593                sendAuthMethNotSupported( ldapSession, bindRequest );
594
595                return;
596            }
597
598            // Store the mechanism in the ldap session
599            ldapSession.putSaslProperty( SaslConstants.SASL_MECH, saslMechanism );
600
601            // Get the handler for this mechanism
602            MechanismHandler mechanismHandler = handlers.get( saslMechanism );
603
604            // Store the mechanism handler in the salsProperties
605            ldapSession.putSaslProperty( SaslConstants.SASL_MECH_HANDLER, mechanismHandler );
606
607            // Initialize the mechanism specific data
608            mechanismHandler.init( ldapSession );
609
610            // Get the SaslServer instance which manage the C/R exchange
611            SaslServer ss = mechanismHandler.handleMechanism( ldapSession, bindRequest );
612
613            // We have to generate a challenge
614            generateSaslChallengeOrComplete( ldapSession, ss, bindRequest );
615
616            // And get back
617        }
618        else if ( ldapSession.isAuthPending() )
619        {
620            try
621            {
622                handleSaslAuthPending( ldapSession, bindRequest );
623            }
624            catch ( SaslException se )
625            {
626                sendInvalidCredentials( ldapSession, ( BindResponse ) bindRequest.getResultResponse(), se );
627            }
628        }
629    }
630
631
632    /**
633     * Deal with a received BindRequest
634     *
635     * @param ldapSession The current session
636     * @param bindRequest The received BindRequest
637     * @throws Exception If the authentication cannot be handled
638     */
639    @Override
640    public void handle( LdapSession ldapSession, BindRequest bindRequest ) throws Exception
641    {
642        LOG.debug( "Received: {}", bindRequest );
643
644        // Guard clause:  LDAP version 3
645        if ( !bindRequest.getVersion3() )
646        {
647            BindResponse bindResponse = ( BindResponse ) bindRequest.getResultResponse();
648
649            LOG.error( I18n.err( I18n.ERR_162 ) );
650            LdapResult bindResult = bindResponse.getLdapResult();
651            bindResult.setResultCode( ResultCodeEnum.PROTOCOL_ERROR );
652            bindResult.setDiagnosticMessage( I18n.err( I18n.ERR_163 ) );
653            ldapSession.getIoSession().write( bindResponse );
654
655            return;
656        }
657
658        // Deal with the two kinds of authentication : Simple and SASL
659        if ( bindRequest.isSimple() )
660        {
661            handleSimpleAuth( ldapSession, bindRequest );
662        }
663        else
664        {
665            handleSaslAuth( ldapSession, bindRequest );
666        }
667    }
668}