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}