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 }