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