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