001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2006-2009 Sun Microsystems, Inc. 025 * Portions Copyright 2012-2015 ForgeRock AS 026 */ 027package org.opends.server.tools; 028 029import java.io.BufferedWriter; 030import java.io.File; 031import java.io.FileWriter; 032import java.io.IOException; 033import java.io.UnsupportedEncodingException; 034import java.security.MessageDigest; 035import java.security.PrivilegedExceptionAction; 036import java.security.SecureRandom; 037import java.util.Arrays; 038import java.util.HashMap; 039import java.util.Iterator; 040import java.util.LinkedHashMap; 041import java.util.LinkedList; 042import java.util.List; 043import java.util.Map; 044import java.util.StringTokenizer; 045import java.util.concurrent.atomic.AtomicInteger; 046 047import javax.security.auth.Subject; 048import javax.security.auth.callback.Callback; 049import javax.security.auth.callback.CallbackHandler; 050import javax.security.auth.callback.NameCallback; 051import javax.security.auth.callback.PasswordCallback; 052import javax.security.auth.callback.UnsupportedCallbackException; 053import javax.security.auth.login.LoginContext; 054import javax.security.sasl.Sasl; 055import javax.security.sasl.SaslClient; 056 057import com.forgerock.opendj.cli.ClientException; 058import com.forgerock.opendj.cli.ConsoleApplication; 059import com.forgerock.opendj.cli.ReturnCode; 060 061import org.forgerock.i18n.LocalizableMessage; 062import org.forgerock.opendj.ldap.ByteSequence; 063import org.forgerock.opendj.ldap.ByteString; 064import org.forgerock.opendj.ldap.DecodeException; 065import org.opends.server.protocols.ldap.BindRequestProtocolOp; 066import org.opends.server.protocols.ldap.BindResponseProtocolOp; 067import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp; 068import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp; 069import org.opends.server.protocols.ldap.LDAPMessage; 070import org.opends.server.types.LDAPException; 071import org.opends.server.types.Control; 072import org.opends.server.util.Base64; 073 074import static org.opends.messages.ToolMessages.*; 075import static org.opends.server.protocols.ldap.LDAPConstants.*; 076import static com.forgerock.opendj.cli.ArgumentConstants.*; 077import static org.opends.server.util.ServerConstants.*; 078import static org.opends.server.util.StaticUtils.*; 079 080 081 082/** 083 * This class provides a generic interface that LDAP clients can use to perform 084 * various kinds of authentication to the Directory Server. This handles both 085 * simple authentication as well as several SASL mechanisms including: 086 * <UL> 087 * <LI>ANONYMOUS</LI> 088 * <LI>CRAM-MD5</LI> 089 * <LI>DIGEST-MD5</LI> 090 * <LI>EXTERNAL</LI> 091 * <LI>GSSAPI</LI> 092 * <LI>PLAIN</LI> 093 * </UL> 094 * <BR><BR> 095 * Note that this implementation is not thread safe, so if the same 096 * <CODE>AuthenticationHandler</CODE> object is to be used concurrently by 097 * multiple threads, it must be externally synchronized. 098 */ 099public class LDAPAuthenticationHandler 100 implements PrivilegedExceptionAction<Object>, CallbackHandler 101{ 102 /** The bind DN for GSSAPI authentication. */ 103 private ByteSequence gssapiBindDN; 104 105 /** The LDAP reader that will be used to read data from the server. */ 106 private final LDAPReader reader; 107 108 /** The LDAP writer that will be used to send data to the server. */ 109 private final LDAPWriter writer; 110 111 /** 112 * The atomic integer that will be used to obtain message IDs for request 113 * messages. 114 */ 115 private final AtomicInteger nextMessageID; 116 117 /** An array filled with the inner pad byte. */ 118 private byte[] iPad; 119 120 /** An array filled with the outer pad byte. */ 121 private byte[] oPad; 122 123 /** The authentication password for GSSAPI authentication. */ 124 private char[] gssapiAuthPW; 125 126 /** The message digest that will be used to create MD5 hashes. */ 127 private MessageDigest md5Digest; 128 129 /** The secure random number generator for use by this authentication handler. */ 130 private SecureRandom secureRandom; 131 132 /** The authentication ID for GSSAPI authentication. */ 133 private String gssapiAuthID; 134 135 /** The authorization ID for GSSAPI authentication. */ 136 private String gssapiAuthzID; 137 138 /** The quality of protection for GSSAPI authentication. */ 139 private String gssapiQoP; 140 141 /** The host name used to connect to the remote system. */ 142 private final String hostName; 143 144 /** The SASL mechanism that will be used for callback authentication. */ 145 private String saslMechanism; 146 147 148 149 /** 150 * Creates a new instance of this authentication handler. All initialization 151 * will be done lazily to avoid unnecessary performance hits, particularly 152 * for cases in which simple authentication will be used as it does not 153 * require any particularly expensive processing. 154 * 155 * @param reader The LDAP reader that will be used to read data from 156 * the server. 157 * @param writer The LDAP writer that will be used to send data to 158 * the server. 159 * @param hostName The host name used to connect to the remote system 160 * (fully-qualified if possible). 161 * @param nextMessageID The atomic integer that will be used to obtain 162 * message IDs for request messages. 163 */ 164 public LDAPAuthenticationHandler(LDAPReader reader, LDAPWriter writer, 165 String hostName, AtomicInteger nextMessageID) 166 { 167 this.reader = reader; 168 this.writer = writer; 169 this.hostName = hostName; 170 this.nextMessageID = nextMessageID; 171 172 md5Digest = null; 173 secureRandom = null; 174 iPad = null; 175 oPad = null; 176 } 177 178 179 180 /** 181 * Retrieves a list of the SASL mechanisms that are supported by this client 182 * library. 183 * 184 * @return A list of the SASL mechanisms that are supported by this client 185 * library. 186 */ 187 public static String[] getSupportedSASLMechanisms() 188 { 189 return new String[] 190 { 191 SASL_MECHANISM_ANONYMOUS, 192 SASL_MECHANISM_CRAM_MD5, 193 SASL_MECHANISM_DIGEST_MD5, 194 SASL_MECHANISM_EXTERNAL, 195 SASL_MECHANISM_GSSAPI, 196 SASL_MECHANISM_PLAIN 197 }; 198 } 199 200 201 202 /** 203 * Retrieves a list of the SASL properties that may be provided for the 204 * specified SASL mechanism, mapped from the property names to their 205 * corresponding descriptions. 206 * 207 * @param mechanism The name of the SASL mechanism for which to obtain the 208 * list of supported properties. 209 * 210 * @return A list of the SASL properties that may be provided for the 211 * specified SASL mechanism, mapped from the property names to their 212 * corresponding descriptions. 213 */ 214 public static LinkedHashMap<String,LocalizableMessage> getSASLProperties( 215 String mechanism) 216 { 217 String upperName = toUpperCase(mechanism); 218 if (upperName.equals(SASL_MECHANISM_ANONYMOUS)) 219 { 220 return getSASLAnonymousProperties(); 221 } 222 else if (upperName.equals(SASL_MECHANISM_CRAM_MD5)) 223 { 224 return getSASLCRAMMD5Properties(); 225 } 226 else if (upperName.equals(SASL_MECHANISM_DIGEST_MD5)) 227 { 228 return getSASLDigestMD5Properties(); 229 } 230 else if (upperName.equals(SASL_MECHANISM_EXTERNAL)) 231 { 232 return getSASLExternalProperties(); 233 } 234 else if (upperName.equals(SASL_MECHANISM_GSSAPI)) 235 { 236 return getSASLGSSAPIProperties(); 237 } 238 else if (upperName.equals(SASL_MECHANISM_PLAIN)) 239 { 240 return getSASLPlainProperties(); 241 } 242 else 243 { 244 // This is an unsupported mechanism. 245 return null; 246 } 247 } 248 249 250 251 /** 252 * Processes a bind using simple authentication with the provided information. 253 * If the bind fails, then an exception will be thrown with information about 254 * the reason for the failure. If the bind is successful but there may be 255 * some special information that the client should be given, then it will be 256 * returned as a String. 257 * 258 * @param ldapVersion The LDAP protocol version to use for the bind 259 * request. 260 * @param bindDN The DN to use to bind to the Directory Server, or 261 * <CODE>null</CODE> if it is to be an anonymous 262 * bind. 263 * @param bindPassword The password to use to bind to the Directory 264 * Server, or <CODE>null</CODE> if it is to be an 265 * anonymous bind. 266 * @param requestControls The set of controls to include the request to the 267 * server. 268 * @param responseControls A list to hold the set of controls included in 269 * the response from the server. 270 * 271 * @return A message providing additional information about the bind if 272 * appropriate, or <CODE>null</CODE> if there is no special 273 * information available. 274 * 275 * @throws ClientException If a client-side problem prevents the bind 276 * attempt from succeeding. 277 * 278 * @throws LDAPException If the bind fails or some other server-side problem 279 * occurs during processing. 280 */ 281 public String doSimpleBind(int ldapVersion, ByteSequence bindDN, 282 ByteSequence bindPassword, 283 List<Control> requestControls, 284 List<Control> responseControls) 285 throws ClientException, LDAPException 286 { 287 //Password is empty, set it to ByteString.empty. 288 if (bindPassword == null) 289 { 290 bindPassword = ByteString.empty(); 291 } 292 293 294 // Make sure that critical elements aren't null. 295 if (bindDN == null) 296 { 297 bindDN = ByteString.empty(); 298 } 299 300 301 // Create the bind request and send it to the server. 302 BindRequestProtocolOp bindRequest = 303 new BindRequestProtocolOp(bindDN.toByteString(), ldapVersion, 304 bindPassword.toByteString()); 305 LDAPMessage bindRequestMessage = 306 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, 307 requestControls); 308 309 try 310 { 311 writer.writeMessage(bindRequestMessage); 312 } 313 catch (IOException ioe) 314 { 315 LocalizableMessage message = 316 ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(ioe)); 317 throw new ClientException( 318 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 319 } 320 catch (Exception e) 321 { 322 LocalizableMessage message = 323 ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(e)); 324 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e); 325 } 326 327 328 // Read the response from the server. 329 LDAPMessage responseMessage; 330 try 331 { 332 responseMessage = reader.readMessage(); 333 if (responseMessage == null) 334 { 335 LocalizableMessage message = 336 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 337 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 338 message); 339 } 340 } 341 catch (DecodeException | LDAPException e) 342 { 343 LocalizableMessage message = 344 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 345 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 346 } 347 catch (IOException ioe) 348 { 349 LocalizableMessage message = 350 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe)); 351 throw new ClientException( 352 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 353 } 354 catch (Exception e) 355 { 356 LocalizableMessage message = 357 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 358 throw new ClientException( 359 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 360 } 361 362 363 // See if there are any controls in the response. If so, then add them to 364 // the response controls list. 365 List<Control> respControls = responseMessage.getControls(); 366 if (respControls != null && !respControls.isEmpty()) 367 { 368 responseControls.addAll(respControls); 369 } 370 371 372 // Look at the protocol op from the response. If it's a bind response, then 373 // continue. If it's an extended response, then it could be a notice of 374 // disconnection so check for that. Otherwise, generate an error. 375 generateError(responseMessage); 376 377 378 BindResponseProtocolOp bindResponse = 379 responseMessage.getBindResponseProtocolOp(); 380 int resultCode = bindResponse.getResultCode(); 381 if (resultCode == ReturnCode.SUCCESS.get()) 382 { 383 // FIXME -- Need to look for things like password expiration warning, 384 // reset notice, etc. 385 return null; 386 } 387 388 // FIXME -- Add support for referrals. 389 390 LocalizableMessage message = ERR_LDAPAUTH_SIMPLE_BIND_FAILED.get(); 391 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), 392 message, bindResponse.getMatchedDN(), null); 393 } 394 395 396 397 /** 398 * Processes a SASL bind using the provided information. If the bind fails, 399 * then an exception will be thrown with information about the reason for the 400 * failure. If the bind is successful but there may be some special 401 * information that the client should be given, then it will be returned as a 402 * String. 403 * 404 * @param bindDN The DN to use to bind to the Directory Server, or 405 * <CODE>null</CODE> if the authentication identity 406 * is to be set through some other means. 407 * @param bindPassword The password to use to bind to the Directory 408 * Server, or <CODE>null</CODE> if this is not a 409 * password-based SASL mechanism. 410 * @param mechanism The name of the SASL mechanism to use to 411 * authenticate to the Directory Server. 412 * @param saslProperties A set of additional properties that may be needed 413 * to process the SASL bind. 414 * @param requestControls The set of controls to include the request to the 415 * server. 416 * @param responseControls A list to hold the set of controls included in 417 * the response from the server. 418 * 419 * @return A message providing additional information about the bind if 420 * appropriate, or <CODE>null</CODE> if there is no special 421 * information available. 422 * 423 * @throws ClientException If a client-side problem prevents the bind 424 * attempt from succeeding. 425 * 426 * @throws LDAPException If the bind fails or some other server-side problem 427 * occurs during processing. 428 */ 429 public String doSASLBind(ByteSequence bindDN, ByteSequence bindPassword, 430 String mechanism, 431 Map<String,List<String>> saslProperties, 432 List<Control> requestControls, 433 List<Control> responseControls) 434 throws ClientException, LDAPException 435 { 436 // Make sure that critical elements aren't null. 437 if (bindDN == null) 438 { 439 bindDN = ByteString.empty(); 440 } 441 442 if (mechanism == null || mechanism.length() == 0) 443 { 444 LocalizableMessage message = ERR_LDAPAUTH_NO_SASL_MECHANISM.get(); 445 throw new ClientException( 446 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 447 } 448 449 450 // Look at the mechanism name and call the appropriate method to process 451 // the request. 452 saslMechanism = toUpperCase(mechanism); 453 if (saslMechanism.equals(SASL_MECHANISM_ANONYMOUS)) 454 { 455 return doSASLAnonymous(bindDN, saslProperties, requestControls, 456 responseControls); 457 } 458 else if (saslMechanism.equals(SASL_MECHANISM_CRAM_MD5)) 459 { 460 return doSASLCRAMMD5(bindDN, bindPassword, saslProperties, 461 requestControls, responseControls); 462 } 463 else if (saslMechanism.equals(SASL_MECHANISM_DIGEST_MD5)) 464 { 465 return doSASLDigestMD5(bindDN, bindPassword, saslProperties, 466 requestControls, responseControls); 467 } 468 else if (saslMechanism.equals(SASL_MECHANISM_EXTERNAL)) 469 { 470 return doSASLExternal(bindDN, saslProperties, requestControls, 471 responseControls); 472 } 473 else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI)) 474 { 475 return doSASLGSSAPI(bindDN, bindPassword, saslProperties, requestControls, 476 responseControls); 477 } 478 else if (saslMechanism.equals(SASL_MECHANISM_PLAIN)) 479 { 480 return doSASLPlain(bindDN, bindPassword, saslProperties, requestControls, 481 responseControls); 482 } 483 else 484 { 485 LocalizableMessage message = ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mechanism); 486 throw new ClientException( 487 ReturnCode.CLIENT_SIDE_AUTH_UNKNOWN, message); 488 } 489 } 490 491 492 493 /** 494 * Processes a SASL ANONYMOUS bind with the provided information. 495 * 496 * @param bindDN The DN to use to bind to the Directory Server, or 497 * <CODE>null</CODE> if the authentication identity 498 * is to be set through some other means. 499 * @param saslProperties A set of additional properties that may be needed 500 * to process the SASL bind. 501 * @param requestControls The set of controls to include the request to the 502 * server. 503 * @param responseControls A list to hold the set of controls included in 504 * the response from the server. 505 * 506 * @return A message providing additional information about the bind if 507 * appropriate, or <CODE>null</CODE> if there is no special 508 * information available. 509 * 510 * @throws ClientException If a client-side problem prevents the bind 511 * attempt from succeeding. 512 * 513 * @throws LDAPException If the bind fails or some other server-side problem 514 * occurs during processing. 515 */ 516 public String doSASLAnonymous(ByteSequence bindDN, 517 Map<String,List<String>> saslProperties, 518 List<Control> requestControls, 519 List<Control> responseControls) 520 throws ClientException, LDAPException 521 { 522 String trace = null; 523 524 525 // Evaluate the properties provided. The only one we'll allow is the trace 526 // property, but it is not required. 527 if (saslProperties == null || saslProperties.isEmpty()) 528 { 529 // This is fine because there are no required properties for this mechanism. 530 } 531 else 532 { 533 for (String name : saslProperties.keySet()) 534 { 535 if (name.equalsIgnoreCase(SASL_PROPERTY_TRACE)) 536 { 537 // This is acceptable, and we'll take any single value. 538 List<String> values = saslProperties.get(name); 539 Iterator<String> iterator = values.iterator(); 540 if (iterator.hasNext()) 541 { 542 trace = iterator.next(); 543 544 if (iterator.hasNext()) 545 { 546 LocalizableMessage message = ERR_LDAPAUTH_TRACE_SINGLE_VALUED.get(); 547 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 548 } 549 } 550 } 551 else 552 { 553 LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get( 554 name, SASL_MECHANISM_ANONYMOUS); 555 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 556 message); 557 } 558 } 559 } 560 561 562 // Construct the bind request and send it to the server. 563 ByteString saslCredentials; 564 if (trace == null) 565 { 566 saslCredentials = null; 567 } 568 else 569 { 570 saslCredentials = ByteString.valueOfUtf8(trace); 571 } 572 573 BindRequestProtocolOp bindRequest = 574 new BindRequestProtocolOp(bindDN.toByteString(), 575 SASL_MECHANISM_ANONYMOUS, saslCredentials); 576 LDAPMessage requestMessage = 577 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, 578 requestControls); 579 580 try 581 { 582 writer.writeMessage(requestMessage); 583 } 584 catch (IOException ioe) 585 { 586 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 587 SASL_MECHANISM_ANONYMOUS, getExceptionMessage(ioe)); 588 throw new ClientException( 589 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 590 } 591 catch (Exception e) 592 { 593 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 594 SASL_MECHANISM_ANONYMOUS, getExceptionMessage(e)); 595 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e); 596 } 597 598 599 // Read the response from the server. 600 LDAPMessage responseMessage; 601 try 602 { 603 responseMessage = reader.readMessage(); 604 if (responseMessage == null) 605 { 606 LocalizableMessage message = 607 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 608 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 609 message); 610 } 611 } 612 catch (DecodeException | LDAPException e) 613 { 614 LocalizableMessage message = 615 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 616 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 617 } 618 catch (IOException ioe) 619 { 620 LocalizableMessage message = 621 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe)); 622 throw new ClientException( 623 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 624 } 625 catch (Exception e) 626 { 627 LocalizableMessage message = 628 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 629 throw new ClientException( 630 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 631 } 632 633 634 // See if there are any controls in the response. If so, then add them to 635 // the response controls list. 636 List<Control> respControls = responseMessage.getControls(); 637 if (respControls != null && ! respControls.isEmpty()) 638 { 639 responseControls.addAll(respControls); 640 } 641 642 643 // Look at the protocol op from the response. If it's a bind response, then 644 // continue. If it's an extended response, then it could be a notice of 645 // disconnection so check for that. Otherwise, generate an error. 646 generateError(responseMessage); 647 648 649 BindResponseProtocolOp bindResponse = 650 responseMessage.getBindResponseProtocolOp(); 651 int resultCode = bindResponse.getResultCode(); 652 if (resultCode == ReturnCode.SUCCESS.get()) 653 { 654 // FIXME -- Need to look for things like password expiration warning, 655 // reset notice, etc. 656 return null; 657 } 658 659 // FIXME -- Add support for referrals. 660 661 LocalizableMessage message = 662 ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_ANONYMOUS); 663 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), 664 message, bindResponse.getMatchedDN(), null); 665 } 666 667 668 669 /** 670 * Retrieves the set of properties that a client may provide when performing a 671 * SASL ANONYMOUS bind, mapped from the property names to their corresponding 672 * descriptions. 673 * 674 * @return The set of properties that a client may provide when performing a 675 * SASL ANONYMOUS bind, mapped from the property names to their 676 * corresponding descriptions. 677 */ 678 public static LinkedHashMap<String, LocalizableMessage> getSASLAnonymousProperties() 679 { 680 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1); 681 682 properties.put(SASL_PROPERTY_TRACE, 683 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_TRACE.get()); 684 685 return properties; 686 } 687 688 689 690 /** 691 * Processes a SASL CRAM-MD5 bind with the provided information. 692 * 693 * @param bindDN The DN to use to bind to the Directory Server, or 694 * <CODE>null</CODE> if the authentication identity 695 * is to be set through some other means. 696 * @param bindPassword The password to use to bind to the Directory 697 * Server. 698 * @param saslProperties A set of additional properties that may be needed 699 * to process the SASL bind. 700 * @param requestControls The set of controls to include the request to the 701 * server. 702 * @param responseControls A list to hold the set of controls included in 703 * the response from the server. 704 * 705 * @return A message providing additional information about the bind if 706 * appropriate, or <CODE>null</CODE> if there is no special 707 * information available. 708 * 709 * @throws ClientException If a client-side problem prevents the bind 710 * attempt from succeeding. 711 * 712 * @throws LDAPException If the bind fails or some other server-side problem 713 * occurs during processing. 714 */ 715 public String doSASLCRAMMD5(ByteSequence bindDN, 716 ByteSequence bindPassword, 717 Map<String,List<String>> saslProperties, 718 List<Control> requestControls, 719 List<Control> responseControls) 720 throws ClientException, LDAPException 721 { 722 String authID = null; 723 724 725 // Evaluate the properties provided. The authID is required, no other 726 // properties are allowed. 727 if (saslProperties == null || saslProperties.isEmpty()) 728 { 729 LocalizableMessage message = 730 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_CRAM_MD5); 731 throw new ClientException( 732 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 733 } 734 735 for (String name : saslProperties.keySet()) 736 { 737 String lowerName = toLowerCase(name); 738 739 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 740 { 741 authID = getAuthID(saslProperties, authID, name); 742 } 743 else 744 { 745 LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get( 746 name, SASL_MECHANISM_CRAM_MD5); 747 throw new ClientException( 748 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 749 } 750 } 751 752 753 // Make sure that the authID was provided. 754 if (authID == null || authID.length() == 0) 755 { 756 LocalizableMessage message = 757 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_CRAM_MD5); 758 throw new ClientException( 759 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 760 } 761 762 763 // Set password to ByteString.empty if the password is null. 764 if (bindPassword == null) 765 { 766 bindPassword = ByteString.empty(); 767 } 768 769 770 // Construct the initial bind request to send to the server. In this case, 771 // we'll simply indicate that we want to use CRAM-MD5 so the server will 772 // send us the challenge. 773 BindRequestProtocolOp bindRequest1 = 774 new BindRequestProtocolOp(bindDN.toByteString(), 775 SASL_MECHANISM_CRAM_MD5, null); 776 // FIXME -- Should we include request controls in both stages or just the 777 // second stage? 778 LDAPMessage requestMessage1 = 779 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1); 780 781 try 782 { 783 writer.writeMessage(requestMessage1); 784 } 785 catch (IOException ioe) 786 { 787 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get( 788 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe)); 789 throw new ClientException( 790 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 791 } 792 catch (Exception e) 793 { 794 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get( 795 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e)); 796 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e); 797 } 798 799 800 // Read the response from the server. 801 LDAPMessage responseMessage1; 802 try 803 { 804 responseMessage1 = reader.readMessage(); 805 if (responseMessage1 == null) 806 { 807 LocalizableMessage message = 808 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 809 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 810 message); 811 } 812 } 813 catch (DecodeException | LDAPException e) 814 { 815 LocalizableMessage message = 816 ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get( 817 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e)); 818 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 819 } 820 catch (IOException ioe) 821 { 822 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get( 823 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe)); 824 throw new ClientException( 825 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 826 } 827 catch (Exception e) 828 { 829 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get( 830 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e)); 831 throw new ClientException( 832 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 833 } 834 835 836 // Look at the protocol op from the response. If it's a bind response, then 837 // continue. If it's an extended response, then it could be a notice of 838 // disconnection so check for that. Otherwise, generate an error. 839 switch (responseMessage1.getProtocolOpType()) 840 { 841 case OP_TYPE_BIND_RESPONSE: 842 // We'll deal with this later. 843 break; 844 845 case OP_TYPE_EXTENDED_RESPONSE: 846 ExtendedResponseProtocolOp extendedResponse = 847 responseMessage1.getExtendedResponseProtocolOp(); 848 String responseOID = extendedResponse.getOID(); 849 if (responseOID != null && 850 responseOID.equals(OID_NOTICE_OF_DISCONNECTION)) 851 { 852 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT. 853 get(extendedResponse.getResultCode(), 854 extendedResponse.getErrorMessage()); 855 throw new LDAPException(extendedResponse.getResultCode(), message); 856 } 857 else 858 { 859 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse); 860 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 861 } 862 863 default: 864 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage1.getProtocolOp()); 865 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 866 } 867 868 869 // Make sure that the bind response has the "SASL bind in progress" result 870 // code. 871 BindResponseProtocolOp bindResponse1 = 872 responseMessage1.getBindResponseProtocolOp(); 873 int resultCode1 = bindResponse1.getResultCode(); 874 if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get()) 875 { 876 LocalizableMessage errorMessage = bindResponse1.getErrorMessage(); 877 if (errorMessage == null) 878 { 879 errorMessage = LocalizableMessage.EMPTY; 880 } 881 882 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE. 883 get(SASL_MECHANISM_CRAM_MD5, resultCode1, 884 ReturnCode.get(resultCode1), errorMessage); 885 throw new LDAPException(resultCode1, errorMessage, message, 886 bindResponse1.getMatchedDN(), null); 887 } 888 889 890 // Make sure that the bind response contains SASL credentials with the 891 // challenge to use for the next stage of the bind. 892 ByteString serverChallenge = bindResponse1.getServerSASLCredentials(); 893 if (serverChallenge == null) 894 { 895 LocalizableMessage message = ERR_LDAPAUTH_NO_CRAMMD5_SERVER_CREDENTIALS.get(); 896 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 897 } 898 899 900 // Use the provided password and credentials to generate the CRAM-MD5 901 // response. 902 StringBuilder buffer = new StringBuilder(); 903 buffer.append(authID); 904 buffer.append(' '); 905 buffer.append(generateCRAMMD5Digest(bindPassword, serverChallenge)); 906 907 908 // Create and send the second bind request to the server. 909 BindRequestProtocolOp bindRequest2 = 910 new BindRequestProtocolOp(bindDN.toByteString(), 911 SASL_MECHANISM_CRAM_MD5, ByteString.valueOfUtf8(buffer.toString())); 912 LDAPMessage requestMessage2 = 913 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2, 914 requestControls); 915 916 try 917 { 918 writer.writeMessage(requestMessage2); 919 } 920 catch (IOException ioe) 921 { 922 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get( 923 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe)); 924 throw new ClientException( 925 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 926 } 927 catch (Exception e) 928 { 929 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get( 930 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e)); 931 throw new ClientException( 932 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 933 } 934 935 936 // Read the response from the server. 937 LDAPMessage responseMessage2; 938 try 939 { 940 responseMessage2 = reader.readMessage(); 941 if (responseMessage2 == null) 942 { 943 LocalizableMessage message = 944 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 945 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 946 message); 947 } 948 } 949 catch (DecodeException | LDAPException e) 950 { 951 LocalizableMessage message = 952 ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get( 953 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e)); 954 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 955 } 956 catch (IOException ioe) 957 { 958 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get( 959 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe)); 960 throw new ClientException( 961 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 962 } 963 catch (Exception e) 964 { 965 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get( 966 SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e)); 967 throw new ClientException( 968 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 969 } 970 971 972 // See if there are any controls in the response. If so, then add them to 973 // the response controls list. 974 List<Control> respControls = responseMessage2.getControls(); 975 if (respControls != null && ! respControls.isEmpty()) 976 { 977 responseControls.addAll(respControls); 978 } 979 980 981 // Look at the protocol op from the response. If it's a bind response, then 982 // continue. If it's an extended response, then it could be a notice of 983 // disconnection so check for that. Otherwise, generate an error. 984 switch (responseMessage2.getProtocolOpType()) 985 { 986 case OP_TYPE_BIND_RESPONSE: 987 // We'll deal with this later. 988 break; 989 990 case OP_TYPE_EXTENDED_RESPONSE: 991 ExtendedResponseProtocolOp extendedResponse = 992 responseMessage2.getExtendedResponseProtocolOp(); 993 String responseOID = extendedResponse.getOID(); 994 if (responseOID != null && 995 responseOID.equals(OID_NOTICE_OF_DISCONNECTION)) 996 { 997 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT. 998 get(extendedResponse.getResultCode(), 999 extendedResponse.getErrorMessage()); 1000 throw new LDAPException(extendedResponse.getResultCode(), message); 1001 } 1002 else 1003 { 1004 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse); 1005 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1006 } 1007 1008 default: 1009 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage2.getProtocolOp()); 1010 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1011 } 1012 1013 1014 BindResponseProtocolOp bindResponse2 = 1015 responseMessage2.getBindResponseProtocolOp(); 1016 int resultCode2 = bindResponse2.getResultCode(); 1017 if (resultCode2 == ReturnCode.SUCCESS.get()) 1018 { 1019 // FIXME -- Need to look for things like password expiration warning, 1020 // reset notice, etc. 1021 return null; 1022 } 1023 1024 // FIXME -- Add support for referrals. 1025 1026 LocalizableMessage message = 1027 ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_CRAM_MD5); 1028 throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(), 1029 message, bindResponse2.getMatchedDN(), null); 1030 } 1031 1032 1033 1034 /** 1035 * @param saslProperties 1036 * @param authID 1037 * @param name 1038 * @return 1039 * @throws ClientException 1040 */ 1041 private String getAuthID(Map<String, List<String>> saslProperties, String authID, String name) throws ClientException 1042 { 1043 List<String> values = saslProperties.get(name); 1044 Iterator<String> iterator = values.iterator(); 1045 if (iterator.hasNext()) 1046 { 1047 authID = iterator.next(); 1048 1049 if (iterator.hasNext()) 1050 { 1051 LocalizableMessage message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get(); 1052 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 1053 } 1054 } 1055 return authID; 1056 } 1057 1058 1059 1060 /** 1061 * Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication 1062 * with the given information. 1063 * 1064 * @param password The clear-text password to use when generating the 1065 * digest. 1066 * @param challenge The server-supplied challenge to use when generating the 1067 * digest. 1068 * 1069 * @return The generated HMAC-MD5 digest for CRAM-MD5 authentication. 1070 * 1071 * @throws ClientException If a problem occurs while attempting to perform 1072 * the necessary initialization. 1073 */ 1074 private String generateCRAMMD5Digest(ByteSequence password, 1075 ByteSequence challenge) 1076 throws ClientException 1077 { 1078 // Perform the necessary initialization if it hasn't been done yet. 1079 if (md5Digest == null) 1080 { 1081 try 1082 { 1083 md5Digest = MessageDigest.getInstance("MD5"); 1084 } 1085 catch (Exception e) 1086 { 1087 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get( 1088 getExceptionMessage(e)); 1089 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 1090 message, e); 1091 } 1092 } 1093 1094 if (iPad == null) 1095 { 1096 iPad = new byte[HMAC_MD5_BLOCK_LENGTH]; 1097 oPad = new byte[HMAC_MD5_BLOCK_LENGTH]; 1098 Arrays.fill(iPad, CRAMMD5_IPAD_BYTE); 1099 Arrays.fill(oPad, CRAMMD5_OPAD_BYTE); 1100 } 1101 1102 1103 // Get the byte arrays backing the password and challenge. 1104 byte[] p = password.toByteArray(); 1105 byte[] c = challenge.toByteArray(); 1106 1107 1108 // If the password is longer than the HMAC-MD5 block length, then use an 1109 // MD5 digest of the password rather than the password itself. 1110 if (password.length() > HMAC_MD5_BLOCK_LENGTH) 1111 { 1112 p = md5Digest.digest(p); 1113 } 1114 1115 1116 // Create byte arrays with data needed for the hash generation. 1117 byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length]; 1118 System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH); 1119 System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length); 1120 1121 byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH]; 1122 System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH); 1123 1124 1125 // Iterate through the bytes in the key and XOR them with the iPad and 1126 // oPad as appropriate. 1127 for (int i=0; i < p.length; i++) 1128 { 1129 iPadAndData[i] ^= p[i]; 1130 oPadAndHash[i] ^= p[i]; 1131 } 1132 1133 1134 // Copy an MD5 digest of the iPad-XORed key and the data into the array to 1135 // be hashed. 1136 System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash, 1137 HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH); 1138 1139 1140 // Calculate an MD5 digest of the resulting array and get the corresponding 1141 // hex string representation. 1142 byte[] digestBytes = md5Digest.digest(oPadAndHash); 1143 1144 StringBuilder hexDigest = new StringBuilder(2*digestBytes.length); 1145 for (byte b : digestBytes) 1146 { 1147 hexDigest.append(byteToLowerHex(b)); 1148 } 1149 1150 return hexDigest.toString(); 1151 } 1152 1153 1154 1155 /** 1156 * Retrieves the set of properties that a client may provide when performing a 1157 * SASL CRAM-MD5 bind, mapped from the property names to their corresponding 1158 * descriptions. 1159 * 1160 * @return The set of properties that a client may provide when performing a 1161 * SASL CRAM-MD5 bind, mapped from the property names to their 1162 * corresponding descriptions. 1163 */ 1164 public static LinkedHashMap<String,LocalizableMessage> getSASLCRAMMD5Properties() 1165 { 1166 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1); 1167 1168 properties.put(SASL_PROPERTY_AUTHID, 1169 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 1170 1171 return properties; 1172 } 1173 1174 1175 1176 /** 1177 * Processes a SASL DIGEST-MD5 bind with the provided information. 1178 * 1179 * @param bindDN The DN to use to bind to the Directory Server, or 1180 * <CODE>null</CODE> if the authentication identity 1181 * is to be set through some other means. 1182 * @param bindPassword The password to use to bind to the Directory 1183 * Server. 1184 * @param saslProperties A set of additional properties that may be needed 1185 * to process the SASL bind. 1186 * @param requestControls The set of controls to include the request to the 1187 * server. 1188 * @param responseControls A list to hold the set of controls included in 1189 * the response from the server. 1190 * 1191 * @return A message providing additional information about the bind if 1192 * appropriate, or <CODE>null</CODE> if there is no special 1193 * information available. 1194 * 1195 * @throws ClientException If a client-side problem prevents the bind 1196 * attempt from succeeding. 1197 * 1198 * @throws LDAPException If the bind fails or some other server-side problem 1199 * occurs during processing. 1200 */ 1201 public String doSASLDigestMD5(ByteSequence bindDN, 1202 ByteSequence bindPassword, 1203 Map<String,List<String>> saslProperties, 1204 List<Control> requestControls, 1205 List<Control> responseControls) 1206 throws ClientException, LDAPException 1207 { 1208 String authID = null; 1209 String realm = null; 1210 String qop = "auth"; 1211 String digestURI = "ldap/" + hostName; 1212 String authzID = null; 1213 boolean realmSetFromProperty = false; 1214 1215 1216 // Evaluate the properties provided. The authID is required. The realm, 1217 // QoP, digest URI, and authzID are optional. 1218 if (saslProperties == null || saslProperties.isEmpty()) 1219 { 1220 LocalizableMessage message = 1221 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_DIGEST_MD5); 1222 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 1223 } 1224 1225 for (String name : saslProperties.keySet()) 1226 { 1227 String lowerName = toLowerCase(name); 1228 1229 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 1230 { 1231 authID = getAuthID(saslProperties, authID, name); 1232 } 1233 else if (lowerName.equals(SASL_PROPERTY_REALM)) 1234 { 1235 List<String> values = saslProperties.get(name); 1236 Iterator<String> iterator = values.iterator(); 1237 if (iterator.hasNext()) 1238 { 1239 realm = iterator.next(); 1240 realmSetFromProperty = true; 1241 1242 if (iterator.hasNext()) 1243 { 1244 LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get(); 1245 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1246 message); 1247 } 1248 } 1249 } 1250 else if (lowerName.equals(SASL_PROPERTY_QOP)) 1251 { 1252 List<String> values = saslProperties.get(name); 1253 Iterator<String> iterator = values.iterator(); 1254 if (iterator.hasNext()) 1255 { 1256 qop = toLowerCase(iterator.next()); 1257 1258 if (iterator.hasNext()) 1259 { 1260 LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get(); 1261 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1262 message); 1263 } 1264 1265 if (qop.equals("auth")) 1266 { 1267 // This is always fine. 1268 } 1269 else if (qop.equals("auth-int") || qop.equals("auth-conf")) 1270 { 1271 // FIXME -- Add support for integrity and confidentiality. 1272 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(qop); 1273 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1274 message); 1275 } 1276 else 1277 { 1278 // This is an illegal value. 1279 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_QOP.get(qop); 1280 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1281 message); 1282 } 1283 } 1284 } 1285 else if (lowerName.equals(SASL_PROPERTY_DIGEST_URI)) 1286 { 1287 List<String> values = saslProperties.get(name); 1288 Iterator<String> iterator = values.iterator(); 1289 if (iterator.hasNext()) 1290 { 1291 digestURI = toLowerCase(iterator.next()); 1292 1293 if (iterator.hasNext()) 1294 { 1295 LocalizableMessage message = ERR_LDAPAUTH_DIGEST_URI_SINGLE_VALUED.get(); 1296 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1297 message); 1298 } 1299 } 1300 } 1301 else if (lowerName.equals(SASL_PROPERTY_AUTHZID)) 1302 { 1303 List<String> values = saslProperties.get(name); 1304 Iterator<String> iterator = values.iterator(); 1305 if (iterator.hasNext()) 1306 { 1307 authzID = toLowerCase(iterator.next()); 1308 1309 if (iterator.hasNext()) 1310 { 1311 LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get(); 1312 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1313 message); 1314 } 1315 } 1316 } 1317 else 1318 { 1319 LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get( 1320 name, SASL_MECHANISM_DIGEST_MD5); 1321 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1322 message); 1323 } 1324 } 1325 1326 1327 // Make sure that the authID was provided. 1328 if (authID == null || authID.length() == 0) 1329 { 1330 LocalizableMessage message = 1331 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_DIGEST_MD5); 1332 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1333 message); 1334 } 1335 1336 1337 // Set password to ByteString.empty if the password is null. 1338 if (bindPassword == null) 1339 { 1340 bindPassword = ByteString.empty(); 1341 } 1342 1343 1344 // Construct the initial bind request to send to the server. In this case, 1345 // we'll simply indicate that we want to use DIGEST-MD5 so the server will 1346 // send us the challenge. 1347 BindRequestProtocolOp bindRequest1 = 1348 new BindRequestProtocolOp(bindDN.toByteString(), 1349 SASL_MECHANISM_DIGEST_MD5, null); 1350 // FIXME -- Should we include request controls in both stages or just the 1351 // second stage? 1352 LDAPMessage requestMessage1 = 1353 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1); 1354 1355 try 1356 { 1357 writer.writeMessage(requestMessage1); 1358 } 1359 catch (IOException ioe) 1360 { 1361 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get( 1362 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe)); 1363 throw new ClientException( 1364 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 1365 } 1366 catch (Exception e) 1367 { 1368 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get( 1369 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e)); 1370 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 1371 message, e); 1372 } 1373 1374 1375 // Read the response from the server. 1376 LDAPMessage responseMessage1; 1377 try 1378 { 1379 responseMessage1 = reader.readMessage(); 1380 if (responseMessage1 == null) 1381 { 1382 LocalizableMessage message = 1383 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 1384 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 1385 message); 1386 } 1387 } 1388 catch (DecodeException | LDAPException e) 1389 { 1390 LocalizableMessage message = 1391 ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get( 1392 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e)); 1393 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 1394 } 1395 catch (IOException ioe) 1396 { 1397 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get( 1398 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe)); 1399 throw new ClientException( 1400 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 1401 } 1402 catch (Exception e) 1403 { 1404 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get( 1405 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e)); 1406 throw new ClientException( 1407 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1408 } 1409 1410 1411 // Look at the protocol op from the response. If it's a bind response, then 1412 // continue. If it's an extended response, then it could be a notice of 1413 // disconnection so check for that. Otherwise, generate an error. 1414 switch (responseMessage1.getProtocolOpType()) 1415 { 1416 case OP_TYPE_BIND_RESPONSE: 1417 // We'll deal with this later. 1418 break; 1419 1420 case OP_TYPE_EXTENDED_RESPONSE: 1421 ExtendedResponseProtocolOp extendedResponse = 1422 responseMessage1.getExtendedResponseProtocolOp(); 1423 String responseOID = extendedResponse.getOID(); 1424 if (responseOID != null && 1425 responseOID.equals(OID_NOTICE_OF_DISCONNECTION)) 1426 { 1427 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT. 1428 get(extendedResponse.getResultCode(), 1429 extendedResponse.getErrorMessage()); 1430 throw new LDAPException(extendedResponse.getResultCode(), message); 1431 } 1432 else 1433 { 1434 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse); 1435 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1436 } 1437 1438 default: 1439 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage1.getProtocolOp()); 1440 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1441 } 1442 1443 1444 // Make sure that the bind response has the "SASL bind in progress" result 1445 // code. 1446 BindResponseProtocolOp bindResponse1 = 1447 responseMessage1.getBindResponseProtocolOp(); 1448 int resultCode1 = bindResponse1.getResultCode(); 1449 if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get()) 1450 { 1451 LocalizableMessage errorMessage = bindResponse1.getErrorMessage(); 1452 if (errorMessage == null) 1453 { 1454 errorMessage = LocalizableMessage.EMPTY; 1455 } 1456 1457 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE. 1458 get(SASL_MECHANISM_DIGEST_MD5, resultCode1, 1459 ReturnCode.get(resultCode1), errorMessage); 1460 throw new LDAPException(resultCode1, errorMessage, message, 1461 bindResponse1.getMatchedDN(), null); 1462 } 1463 1464 1465 // Make sure that the bind response contains SASL credentials with the 1466 // information to use for the next stage of the bind. 1467 ByteString serverCredentials = 1468 bindResponse1.getServerSASLCredentials(); 1469 if (serverCredentials == null) 1470 { 1471 LocalizableMessage message = ERR_LDAPAUTH_NO_DIGESTMD5_SERVER_CREDENTIALS.get(); 1472 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1473 } 1474 1475 1476 // Parse the server SASL credentials to get the necessary information. In 1477 // particular, look at the realm, the nonce, the QoP modes, and the charset. 1478 // We'll only care about the realm if none was provided in the SASL 1479 // properties and only one was provided in the server SASL credentials. 1480 String credString = serverCredentials.toString(); 1481 String lowerCreds = toLowerCase(credString); 1482 String nonce = null; 1483 boolean useUTF8 = false; 1484 int pos = 0; 1485 int length = credString.length(); 1486 while (pos < length) 1487 { 1488 int equalPos = credString.indexOf('=', pos+1); 1489 if (equalPos < 0) 1490 { 1491 // This is bad because we're not at the end of the string but we don't 1492 // have a name/value delimiter. 1493 LocalizableMessage message = 1494 ERR_LDAPAUTH_DIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get( 1495 credString, pos); 1496 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1497 } 1498 1499 1500 String tokenName = lowerCreds.substring(pos, equalPos); 1501 1502 StringBuilder valueBuffer = new StringBuilder(); 1503 pos = readToken(credString, equalPos+1, length, valueBuffer); 1504 String tokenValue = valueBuffer.toString(); 1505 1506 if (tokenName.equals("charset")) 1507 { 1508 // The value must be the string "utf-8". If not, that's an error. 1509 if (! tokenValue.equalsIgnoreCase("utf-8")) 1510 { 1511 LocalizableMessage message = 1512 ERR_LDAPAUTH_DIGESTMD5_INVALID_CHARSET.get(tokenValue); 1513 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1514 } 1515 1516 useUTF8 = true; 1517 } 1518 else if (tokenName.equals("realm")) 1519 { 1520 // This will only be of interest to us if there is only a single realm 1521 // in the server credentials and none was provided as a client-side 1522 // property. 1523 if (! realmSetFromProperty) 1524 { 1525 if (realm == null) 1526 { 1527 // No other realm was specified, so we'll use this one for now. 1528 realm = tokenValue; 1529 } 1530 else 1531 { 1532 // This must mean that there are multiple realms in the server 1533 // credentials. In that case, we'll not provide any realm at all. 1534 // To make sure that happens, pretend that the client specified the 1535 // realm. 1536 realm = null; 1537 realmSetFromProperty = true; 1538 } 1539 } 1540 } 1541 else if (tokenName.equals("nonce")) 1542 { 1543 nonce = tokenValue; 1544 } 1545 else if (tokenName.equals("qop")) 1546 { 1547 // The QoP modes provided by the server should be a comma-delimited 1548 // list. Decode that list and make sure the QoP we have chosen is in 1549 // that list. 1550 StringTokenizer tokenizer = new StringTokenizer(tokenValue, ","); 1551 LinkedList<String> qopModes = new LinkedList<>(); 1552 while (tokenizer.hasMoreTokens()) 1553 { 1554 qopModes.add(toLowerCase(tokenizer.nextToken().trim())); 1555 } 1556 1557 if (! qopModes.contains(qop)) 1558 { 1559 LocalizableMessage message = ERR_LDAPAUTH_REQUESTED_QOP_NOT_SUPPORTED_BY_SERVER. 1560 get(qop, tokenValue); 1561 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1562 message); 1563 } 1564 } 1565 else 1566 { 1567 // Other values may have been provided, but they aren't of interest to 1568 // us because they shouldn't change anything about the way we encode the 1569 // second part of the request. Rather than attempt to examine them, 1570 // we'll assume that the server sent a valid response. 1571 } 1572 } 1573 1574 1575 // Make sure that the nonce was included in the response from the server. 1576 if (nonce == null) 1577 { 1578 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_NONCE.get(); 1579 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1580 } 1581 1582 1583 // Generate the cnonce that we will use for this request. 1584 String cnonce = generateCNonce(); 1585 1586 1587 // Generate the response digest, and initialize the necessary remaining 1588 // variables to use in the generation of that digest. 1589 String nonceCount = "00000001"; 1590 String charset = useUTF8 ? "UTF-8" : "ISO-8859-1"; 1591 String responseDigest; 1592 try 1593 { 1594 responseDigest = generateDigestMD5Response(authID, authzID, 1595 bindPassword, realm, 1596 nonce, cnonce, nonceCount, 1597 digestURI, qop, charset); 1598 } 1599 catch (ClientException ce) 1600 { 1601 throw ce; 1602 } 1603 catch (Exception e) 1604 { 1605 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_CANNOT_CREATE_RESPONSE_DIGEST. 1606 get(getExceptionMessage(e)); 1607 throw new ClientException( 1608 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1609 } 1610 1611 1612 // Generate the SASL credentials for the second bind request. 1613 StringBuilder credBuffer = new StringBuilder(); 1614 credBuffer.append("username=\""); 1615 credBuffer.append(authID); 1616 credBuffer.append("\""); 1617 1618 if (realm != null) 1619 { 1620 credBuffer.append(",realm=\""); 1621 credBuffer.append(realm); 1622 credBuffer.append("\""); 1623 } 1624 1625 credBuffer.append(",nonce=\""); 1626 credBuffer.append(nonce); 1627 credBuffer.append("\",cnonce=\""); 1628 credBuffer.append(cnonce); 1629 credBuffer.append("\",nc="); 1630 credBuffer.append(nonceCount); 1631 credBuffer.append(",qop="); 1632 credBuffer.append(qop); 1633 credBuffer.append(",digest-uri=\""); 1634 credBuffer.append(digestURI); 1635 credBuffer.append("\",response="); 1636 credBuffer.append(responseDigest); 1637 1638 if (useUTF8) 1639 { 1640 credBuffer.append(",charset=utf-8"); 1641 } 1642 1643 if (authzID != null) 1644 { 1645 credBuffer.append(",authzid=\""); 1646 credBuffer.append(authzID); 1647 credBuffer.append("\""); 1648 } 1649 1650 1651 // Generate and send the second bind request. 1652 BindRequestProtocolOp bindRequest2 = 1653 new BindRequestProtocolOp(bindDN.toByteString(), 1654 SASL_MECHANISM_DIGEST_MD5, 1655 ByteString.valueOfUtf8(credBuffer.toString())); 1656 LDAPMessage requestMessage2 = 1657 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2, 1658 requestControls); 1659 1660 try 1661 { 1662 writer.writeMessage(requestMessage2); 1663 } 1664 catch (IOException ioe) 1665 { 1666 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get( 1667 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe)); 1668 throw new ClientException( 1669 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 1670 } 1671 catch (Exception e) 1672 { 1673 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get( 1674 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e)); 1675 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 1676 message, e); 1677 } 1678 1679 1680 // Read the response from the server. 1681 LDAPMessage responseMessage2; 1682 try 1683 { 1684 responseMessage2 = reader.readMessage(); 1685 if (responseMessage2 == null) 1686 { 1687 LocalizableMessage message = 1688 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 1689 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 1690 message); 1691 } 1692 } 1693 catch (DecodeException | LDAPException e) 1694 { 1695 LocalizableMessage message = 1696 ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get( 1697 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e)); 1698 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 1699 } 1700 catch (IOException ioe) 1701 { 1702 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get( 1703 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe)); 1704 throw new ClientException( 1705 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 1706 } 1707 catch (Exception e) 1708 { 1709 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get( 1710 SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e)); 1711 throw new ClientException( 1712 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1713 } 1714 1715 1716 // See if there are any controls in the response. If so, then add them to 1717 // the response controls list. 1718 List<Control> respControls = responseMessage2.getControls(); 1719 if (respControls != null && ! respControls.isEmpty()) 1720 { 1721 responseControls.addAll(respControls); 1722 } 1723 1724 1725 // Look at the protocol op from the response. If it's a bind response, then 1726 // continue. If it's an extended response, then it could be a notice of 1727 // disconnection so check for that. Otherwise, generate an error. 1728 switch (responseMessage2.getProtocolOpType()) 1729 { 1730 case OP_TYPE_BIND_RESPONSE: 1731 // We'll deal with this later. 1732 break; 1733 1734 case OP_TYPE_EXTENDED_RESPONSE: 1735 ExtendedResponseProtocolOp extendedResponse = 1736 responseMessage2.getExtendedResponseProtocolOp(); 1737 String responseOID = extendedResponse.getOID(); 1738 if (responseOID != null && 1739 responseOID.equals(OID_NOTICE_OF_DISCONNECTION)) 1740 { 1741 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT. 1742 get(extendedResponse.getResultCode(), 1743 extendedResponse.getErrorMessage()); 1744 throw new LDAPException(extendedResponse.getResultCode(), message); 1745 } 1746 else 1747 { 1748 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse); 1749 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1750 } 1751 1752 default: 1753 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage2.getProtocolOp()); 1754 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1755 } 1756 1757 1758 BindResponseProtocolOp bindResponse2 = 1759 responseMessage2.getBindResponseProtocolOp(); 1760 int resultCode2 = bindResponse2.getResultCode(); 1761 if (resultCode2 != ReturnCode.SUCCESS.get()) 1762 { 1763 // FIXME -- Add support for referrals. 1764 1765 LocalizableMessage message = 1766 ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_DIGEST_MD5); 1767 throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(), 1768 message, bindResponse2.getMatchedDN(), 1769 null); 1770 } 1771 1772 1773 // Make sure that the bind response included server SASL credentials with 1774 // the appropriate rspauth value. 1775 ByteString rspAuthCreds = bindResponse2.getServerSASLCredentials(); 1776 if (rspAuthCreds == null) 1777 { 1778 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get(); 1779 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1780 } 1781 1782 String credStr = toLowerCase(rspAuthCreds.toString()); 1783 if (! credStr.startsWith("rspauth=")) 1784 { 1785 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get(); 1786 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1787 } 1788 1789 1790 byte[] serverRspAuth; 1791 try 1792 { 1793 serverRspAuth = hexStringToByteArray(credStr.substring(8)); 1794 } 1795 catch (Exception e) 1796 { 1797 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_DECODE_RSPAUTH.get( 1798 getExceptionMessage(e)); 1799 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1800 } 1801 1802 byte[] clientRspAuth; 1803 try 1804 { 1805 clientRspAuth = 1806 generateDigestMD5RspAuth(authID, authzID, bindPassword, 1807 realm, nonce, cnonce, nonceCount, digestURI, 1808 qop, charset); 1809 } 1810 catch (Exception e) 1811 { 1812 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_CALCULATE_RSPAUTH.get( 1813 getExceptionMessage(e)); 1814 throw new ClientException( 1815 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1816 } 1817 1818 if (! Arrays.equals(serverRspAuth, clientRspAuth)) 1819 { 1820 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_RSPAUTH_MISMATCH.get(); 1821 throw new ClientException( 1822 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1823 } 1824 1825 // FIXME -- Need to look for things like password expiration warning, 1826 // reset notice, etc. 1827 return null; 1828 } 1829 1830 1831 1832 /** 1833 * Reads the next token from the provided credentials string using the 1834 * provided information. If the token is surrounded by quotation marks, then 1835 * the token returned will not include those quotation marks. 1836 * 1837 * @param credentials The credentials string from which to read the token. 1838 * @param startPos The position of the first character of the token to 1839 * read. 1840 * @param length The total number of characters in the credentials 1841 * string. 1842 * @param token The buffer into which the token is to be placed. 1843 * 1844 * @return The position at which the next token should start, or a value 1845 * greater than or equal to the length of the string if there are no 1846 * more tokens. 1847 * 1848 * @throws LDAPException If a problem occurs while attempting to read the 1849 * token. 1850 */ 1851 private int readToken(String credentials, int startPos, int length, 1852 StringBuilder token) 1853 throws LDAPException 1854 { 1855 // If the position is greater than or equal to the length, then we shouldn't 1856 // do anything. 1857 if (startPos >= length) 1858 { 1859 return startPos; 1860 } 1861 1862 1863 // Look at the first character to see if it's an empty string or the string 1864 // is quoted. 1865 boolean isEscaped = false; 1866 boolean isQuoted = false; 1867 int pos = startPos; 1868 char c = credentials.charAt(pos++); 1869 1870 if (c == ',') 1871 { 1872 // This must be a zero-length token, so we'll just return the next 1873 // position. 1874 return pos; 1875 } 1876 else if (c == '"') 1877 { 1878 // The string is quoted, so we'll ignore this character, and we'll keep 1879 // reading until we find the unescaped closing quote followed by a comma 1880 // or the end of the string. 1881 isQuoted = true; 1882 } 1883 else if (c == '\\') 1884 { 1885 // The next character is escaped, so we'll take it no matter what. 1886 isEscaped = true; 1887 } 1888 else 1889 { 1890 // The string is not quoted, and this is the first character. Store this 1891 // character and keep reading until we find a comma or the end of the 1892 // string. 1893 token.append(c); 1894 } 1895 1896 1897 // Enter a loop, reading until we find the appropriate criteria for the end 1898 // of the token. 1899 while (pos < length) 1900 { 1901 c = credentials.charAt(pos++); 1902 1903 if (isEscaped) 1904 { 1905 // The previous character was an escape, so we'll take this no matter 1906 // what. 1907 token.append(c); 1908 isEscaped = false; 1909 } 1910 else if (c == ',') 1911 { 1912 // If this is a quoted string, then this comma is part of the token. 1913 // Otherwise, it's the end of the token. 1914 if (isQuoted) 1915 { 1916 token.append(c); 1917 } 1918 else 1919 { 1920 break; 1921 } 1922 } 1923 else if (c == '"') 1924 { 1925 if (isQuoted) 1926 { 1927 // This should be the end of the token, but in order for it to be 1928 // valid it must be followed by a comma or the end of the string. 1929 if (pos >= length) 1930 { 1931 // We have hit the end of the string, so this is fine. 1932 break; 1933 } 1934 else 1935 { 1936 char c2 = credentials.charAt(pos++); 1937 if (c2 == ',') 1938 { 1939 // We have hit the end of the token, so this is fine. 1940 break; 1941 } 1942 else 1943 { 1944 // We found the closing quote before the end of the token. This 1945 // is not fine. 1946 LocalizableMessage message = 1947 ERR_LDAPAUTH_DIGESTMD5_INVALID_CLOSING_QUOTE_POS.get(pos-2); 1948 throw new LDAPException(ReturnCode.INVALID_CREDENTIALS.get(), 1949 message); 1950 } 1951 } 1952 } 1953 else 1954 { 1955 // This must be part of the value, so we'll take it. 1956 token.append(c); 1957 } 1958 } 1959 else if (c == '\\') 1960 { 1961 // The next character is escaped. We'll set a flag so we know to 1962 // accept it, but will not include the backspace itself. 1963 isEscaped = true; 1964 } 1965 else 1966 { 1967 token.append(c); 1968 } 1969 } 1970 1971 1972 return pos; 1973 } 1974 1975 1976 1977 /** 1978 * Generates a cnonce value to use during the DIGEST-MD5 authentication 1979 * process. 1980 * 1981 * @return The cnonce that should be used for DIGEST-MD5 authentication. 1982 */ 1983 private String generateCNonce() 1984 { 1985 if (secureRandom == null) 1986 { 1987 secureRandom = new SecureRandom(); 1988 } 1989 1990 byte[] cnonceBytes = new byte[16]; 1991 secureRandom.nextBytes(cnonceBytes); 1992 1993 return Base64.encode(cnonceBytes); 1994 } 1995 1996 1997 1998 /** 1999 * Generates the appropriate DIGEST-MD5 response for the provided set of 2000 * information. 2001 * 2002 * @param authID The username from the authentication request. 2003 * @param authzID The authorization ID from the request, or 2004 * <CODE>null</CODE> if there is none. 2005 * @param password The clear-text password for the user. 2006 * @param realm The realm for which the authentication is to be 2007 * performed. 2008 * @param nonce The random data generated by the server for use in the 2009 * digest. 2010 * @param cnonce The random data generated by the client for use in the 2011 * digest. 2012 * @param nonceCount The 8-digit hex string indicating the number of times 2013 * the provided nonce has been used by the client. 2014 * @param digestURI The digest URI that specifies the service and host for 2015 * which the authentication is being performed. 2016 * @param qop The quality of protection string for the 2017 * authentication. 2018 * @param charset The character set used to encode the information. 2019 * 2020 * @return The DIGEST-MD5 response for the provided set of information. 2021 * 2022 * @throws ClientException If a problem occurs while attempting to 2023 * initialize the MD5 digest. 2024 * 2025 * @throws UnsupportedEncodingException If the specified character set is 2026 * invalid for some reason. 2027 */ 2028 private String generateDigestMD5Response(String authID, String authzID, 2029 ByteSequence password, String realm, 2030 String nonce, String cnonce, 2031 String nonceCount, String digestURI, 2032 String qop, String charset) 2033 throws ClientException, UnsupportedEncodingException 2034 { 2035 // Perform the necessary initialization if it hasn't been done yet. 2036 if (md5Digest == null) 2037 { 2038 try 2039 { 2040 md5Digest = MessageDigest.getInstance("MD5"); 2041 } 2042 catch (Exception e) 2043 { 2044 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get( 2045 getExceptionMessage(e)); 2046 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 2047 message, e); 2048 } 2049 } 2050 2051 2052 // Get a hash of "username:realm:password". 2053 StringBuilder a1String1 = new StringBuilder(); 2054 a1String1.append(authID); 2055 a1String1.append(':'); 2056 a1String1.append((realm == null) ? "" : realm); 2057 a1String1.append(':'); 2058 2059 byte[] a1Bytes1a = a1String1.toString().getBytes(charset); 2060 byte[] a1Bytes1 = new byte[a1Bytes1a.length + password.length()]; 2061 System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length); 2062 password.copyTo(a1Bytes1, a1Bytes1a.length); 2063 byte[] urpHash = md5Digest.digest(a1Bytes1); 2064 2065 2066 // Next, get a hash of "urpHash:nonce:cnonce[:authzid]". 2067 StringBuilder a1String2 = new StringBuilder(); 2068 a1String2.append(':'); 2069 a1String2.append(nonce); 2070 a1String2.append(':'); 2071 a1String2.append(cnonce); 2072 if (authzID != null) 2073 { 2074 a1String2.append(':'); 2075 a1String2.append(authzID); 2076 } 2077 byte[] a1Bytes2a = a1String2.toString().getBytes(charset); 2078 byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length]; 2079 System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length); 2080 System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length); 2081 byte[] a1Hash = md5Digest.digest(a1Bytes2); 2082 2083 2084 // Next, get a hash of "AUTHENTICATE:digesturi". 2085 byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset); 2086 byte[] a2Hash = md5Digest.digest(a2Bytes); 2087 2088 2089 // Get hex string representations of the last two hashes. 2090 String a1HashHex = getHexString(a1Hash); 2091 String a2HashHex = getHexString(a2Hash); 2092 2093 2094 // Put together the final string to hash, consisting of 2095 // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest. 2096 StringBuilder kdStr = new StringBuilder(); 2097 kdStr.append(a1HashHex); 2098 kdStr.append(':'); 2099 kdStr.append(nonce); 2100 kdStr.append(':'); 2101 kdStr.append(nonceCount); 2102 kdStr.append(':'); 2103 kdStr.append(cnonce); 2104 kdStr.append(':'); 2105 kdStr.append(qop); 2106 kdStr.append(':'); 2107 kdStr.append(a2HashHex); 2108 2109 return getHexString(md5Digest.digest(kdStr.toString().getBytes(charset))); 2110 } 2111 2112 2113 2114 /** 2115 * Generates the appropriate DIGEST-MD5 rspauth digest using the provided 2116 * information. 2117 * 2118 * @param authID The username from the authentication request. 2119 * @param authzID The authorization ID from the request, or 2120 * <CODE>null</CODE> if there is none. 2121 * @param password The clear-text password for the user. 2122 * @param realm The realm for which the authentication is to be 2123 * performed. 2124 * @param nonce The random data generated by the server for use in the 2125 * digest. 2126 * @param cnonce The random data generated by the client for use in the 2127 * digest. 2128 * @param nonceCount The 8-digit hex string indicating the number of times 2129 * the provided nonce has been used by the client. 2130 * @param digestURI The digest URI that specifies the service and host for 2131 * which the authentication is being performed. 2132 * @param qop The quality of protection string for the 2133 * authentication. 2134 * @param charset The character set used to encode the information. 2135 * 2136 * @return The DIGEST-MD5 response for the provided set of information. 2137 * 2138 * @throws UnsupportedEncodingException If the specified character set is 2139 * invalid for some reason. 2140 */ 2141 public byte[] generateDigestMD5RspAuth(String authID, String authzID, 2142 ByteSequence password, String realm, 2143 String nonce, String cnonce, 2144 String nonceCount, String digestURI, 2145 String qop, String charset) 2146 throws UnsupportedEncodingException 2147 { 2148 // First, get a hash of "username:realm:password". 2149 StringBuilder a1String1 = new StringBuilder(); 2150 a1String1.append(authID); 2151 a1String1.append(':'); 2152 a1String1.append(realm); 2153 a1String1.append(':'); 2154 2155 byte[] a1Bytes1a = a1String1.toString().getBytes(charset); 2156 byte[] a1Bytes1 = new byte[a1Bytes1a.length + password.length()]; 2157 System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length); 2158 password.copyTo(a1Bytes1, a1Bytes1a.length); 2159 byte[] urpHash = md5Digest.digest(a1Bytes1); 2160 2161 2162 // Next, get a hash of "urpHash:nonce:cnonce[:authzid]". 2163 StringBuilder a1String2 = new StringBuilder(); 2164 a1String2.append(':'); 2165 a1String2.append(nonce); 2166 a1String2.append(':'); 2167 a1String2.append(cnonce); 2168 if (authzID != null) 2169 { 2170 a1String2.append(':'); 2171 a1String2.append(authzID); 2172 } 2173 byte[] a1Bytes2a = a1String2.toString().getBytes(charset); 2174 byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length]; 2175 System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length); 2176 System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, 2177 a1Bytes2a.length); 2178 byte[] a1Hash = md5Digest.digest(a1Bytes2); 2179 2180 2181 // Next, get a hash of "AUTHENTICATE:digesturi". 2182 String a2String = ":" + digestURI; 2183 if (qop.equals("auth-int") || qop.equals("auth-conf")) 2184 { 2185 a2String += ":00000000000000000000000000000000"; 2186 } 2187 byte[] a2Bytes = a2String.getBytes(charset); 2188 byte[] a2Hash = md5Digest.digest(a2Bytes); 2189 2190 2191 // Get hex string representations of the last two hashes. 2192 String a1HashHex = getHexString(a1Hash); 2193 String a2HashHex = getHexString(a2Hash); 2194 2195 2196 // Put together the final string to hash, consisting of 2197 // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest. 2198 StringBuilder kdStr = new StringBuilder(); 2199 kdStr.append(a1HashHex); 2200 kdStr.append(':'); 2201 kdStr.append(nonce); 2202 kdStr.append(':'); 2203 kdStr.append(nonceCount); 2204 kdStr.append(':'); 2205 kdStr.append(cnonce); 2206 kdStr.append(':'); 2207 kdStr.append(qop); 2208 kdStr.append(':'); 2209 kdStr.append(a2HashHex); 2210 return md5Digest.digest(kdStr.toString().getBytes(charset)); 2211 } 2212 2213 2214 2215 /** 2216 * Retrieves a hexadecimal string representation of the contents of the 2217 * provided byte array. 2218 * 2219 * @param byteArray The byte array for which to obtain the hexadecimal 2220 * string representation. 2221 * 2222 * @return The hexadecimal string representation of the contents of the 2223 * provided byte array. 2224 */ 2225 private String getHexString(byte[] byteArray) 2226 { 2227 StringBuilder buffer = new StringBuilder(2*byteArray.length); 2228 for (byte b : byteArray) 2229 { 2230 buffer.append(byteToLowerHex(b)); 2231 } 2232 2233 return buffer.toString(); 2234 } 2235 2236 2237 2238 /** 2239 * Retrieves the set of properties that a client may provide when performing a 2240 * SASL DIGEST-MD5 bind, mapped from the property names to their corresponding 2241 * descriptions. 2242 * 2243 * @return The set of properties that a client may provide when performing a 2244 * SASL DIGEST-MD5 bind, mapped from the property names to their 2245 * corresponding descriptions. 2246 */ 2247 public static LinkedHashMap<String,LocalizableMessage> getSASLDigestMD5Properties() 2248 { 2249 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(5); 2250 2251 properties.put(SASL_PROPERTY_AUTHID, 2252 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 2253 properties.put(SASL_PROPERTY_REALM, 2254 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get()); 2255 properties.put(SASL_PROPERTY_QOP, 2256 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_QOP.get()); 2257 properties.put(SASL_PROPERTY_DIGEST_URI, 2258 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_DIGEST_URI.get()); 2259 properties.put(SASL_PROPERTY_AUTHZID, 2260 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get()); 2261 2262 return properties; 2263 } 2264 2265 2266 2267 /** 2268 * Processes a SASL EXTERNAL bind with the provided information. 2269 * 2270 * @param bindDN The DN to use to bind to the Directory Server, or 2271 * <CODE>null</CODE> if the authentication identity 2272 * is to be set through some other means. 2273 * @param saslProperties A set of additional properties that may be needed 2274 * to process the SASL bind. SASL EXTERNAL does not 2275 * take any properties, so this should be empty or 2276 * <CODE>null</CODE>. 2277 * @param requestControls The set of controls to include the request to the 2278 * server. 2279 * @param responseControls A list to hold the set of controls included in 2280 * the response from the server. 2281 * 2282 * @return A message providing additional information about the bind if 2283 * appropriate, or <CODE>null</CODE> if there is no special 2284 * information available. 2285 * 2286 * @throws ClientException If a client-side problem prevents the bind 2287 * attempt from succeeding. 2288 * 2289 * @throws LDAPException If the bind fails or some other server-side problem 2290 * occurs during processing. 2291 */ 2292 public String doSASLExternal(ByteSequence bindDN, 2293 Map<String,List<String>> saslProperties, 2294 List<Control> requestControls, 2295 List<Control> responseControls) 2296 throws ClientException, LDAPException 2297 { 2298 // Make sure that no SASL properties were provided. 2299 if (saslProperties != null && ! saslProperties.isEmpty()) 2300 { 2301 LocalizableMessage message = 2302 ERR_LDAPAUTH_NO_ALLOWED_SASL_PROPERTIES.get(SASL_MECHANISM_EXTERNAL); 2303 throw new ClientException( 2304 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2305 } 2306 2307 2308 // Construct the bind request and send it to the server. 2309 BindRequestProtocolOp bindRequest = 2310 new BindRequestProtocolOp(bindDN.toByteString(), 2311 SASL_MECHANISM_EXTERNAL, null); 2312 LDAPMessage requestMessage = 2313 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, 2314 requestControls); 2315 2316 try 2317 { 2318 writer.writeMessage(requestMessage); 2319 } 2320 catch (IOException ioe) 2321 { 2322 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 2323 SASL_MECHANISM_EXTERNAL, getExceptionMessage(ioe)); 2324 throw new ClientException( 2325 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 2326 } 2327 catch (Exception e) 2328 { 2329 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 2330 SASL_MECHANISM_EXTERNAL, getExceptionMessage(e)); 2331 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 2332 message, e); 2333 } 2334 2335 2336 // Read the response from the server. 2337 LDAPMessage responseMessage; 2338 try 2339 { 2340 responseMessage = reader.readMessage(); 2341 if (responseMessage == null) 2342 { 2343 LocalizableMessage message = 2344 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 2345 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 2346 message); 2347 } 2348 } 2349 catch (DecodeException | LDAPException e) 2350 { 2351 LocalizableMessage message = 2352 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 2353 throw new ClientException( 2354 ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 2355 } 2356 catch (IOException ioe) 2357 { 2358 LocalizableMessage message = 2359 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe)); 2360 throw new ClientException( 2361 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 2362 } 2363 catch (Exception e) 2364 { 2365 LocalizableMessage message = 2366 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 2367 throw new ClientException( 2368 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2369 } 2370 2371 2372 // See if there are any controls in the response. If so, then add them to 2373 // the response controls list. 2374 List<Control> respControls = responseMessage.getControls(); 2375 if (respControls != null && ! respControls.isEmpty()) 2376 { 2377 responseControls.addAll(respControls); 2378 } 2379 2380 2381 // Look at the protocol op from the response. If it's a bind response, then 2382 // continue. If it's an extended response, then it could be a notice of 2383 // disconnection so check for that. Otherwise, generate an error. 2384 switch (responseMessage.getProtocolOpType()) 2385 { 2386 case OP_TYPE_BIND_RESPONSE: 2387 // We'll deal with this later. 2388 break; 2389 2390 case OP_TYPE_EXTENDED_RESPONSE: 2391 ExtendedResponseProtocolOp extendedResponse = 2392 responseMessage.getExtendedResponseProtocolOp(); 2393 String responseOID = extendedResponse.getOID(); 2394 if (responseOID != null && 2395 responseOID.equals(OID_NOTICE_OF_DISCONNECTION)) 2396 { 2397 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT. 2398 get(extendedResponse.getResultCode(), 2399 extendedResponse.getErrorMessage()); 2400 throw new LDAPException(extendedResponse.getResultCode(), message); 2401 } 2402 else 2403 { 2404 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse); 2405 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2406 } 2407 2408 default: 2409 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp()); 2410 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2411 } 2412 2413 2414 BindResponseProtocolOp bindResponse = 2415 responseMessage.getBindResponseProtocolOp(); 2416 int resultCode = bindResponse.getResultCode(); 2417 if (resultCode == ReturnCode.SUCCESS.get()) 2418 { 2419 // FIXME -- Need to look for things like password expiration warning, 2420 // reset notice, etc. 2421 return null; 2422 } 2423 2424 // FIXME -- Add support for referrals. 2425 2426 LocalizableMessage message = 2427 ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_EXTERNAL); 2428 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), 2429 message, bindResponse.getMatchedDN(), null); 2430 } 2431 2432 2433 2434 /** 2435 * Retrieves the set of properties that a client may provide when performing a 2436 * SASL EXTERNAL bind, mapped from the property names to their corresponding 2437 * descriptions. 2438 * 2439 * @return The set of properties that a client may provide when performing a 2440 * SASL EXTERNAL bind, mapped from the property names to their 2441 * corresponding descriptions. 2442 */ 2443 public static LinkedHashMap<String,LocalizableMessage> getSASLExternalProperties() 2444 { 2445 // There are no properties for the SASL EXTERNAL mechanism. 2446 return new LinkedHashMap<>(0); 2447 } 2448 2449 2450 2451 /** 2452 * Processes a SASL GSSAPI bind with the provided information. 2453 * 2454 * @param bindDN The DN to use to bind to the Directory Server, or 2455 * <CODE>null</CODE> if the authentication identity 2456 * is to be set through some other means. 2457 * @param bindPassword The password to use to bind to the Directory 2458 * Server. 2459 * @param saslProperties A set of additional properties that may be needed 2460 * to process the SASL bind. SASL EXTERNAL does not 2461 * take any properties, so this should be empty or 2462 * <CODE>null</CODE>. 2463 * @param requestControls The set of controls to include the request to the 2464 * server. 2465 * @param responseControls A list to hold the set of controls included in 2466 * the response from the server. 2467 * 2468 * @return A message providing additional information about the bind if 2469 * appropriate, or <CODE>null</CODE> if there is no special 2470 * information available. 2471 * 2472 * @throws ClientException If a client-side problem prevents the bind 2473 * attempt from succeeding. 2474 * 2475 * @throws LDAPException If the bind fails or some other server-side problem 2476 * occurs during processing. 2477 */ 2478 public String doSASLGSSAPI(ByteSequence bindDN, 2479 ByteSequence bindPassword, 2480 Map<String,List<String>> saslProperties, 2481 List<Control> requestControls, 2482 List<Control> responseControls) 2483 throws ClientException, LDAPException 2484 { 2485 String kdc = null; 2486 String realm = null; 2487 2488 gssapiBindDN = bindDN; 2489 gssapiAuthID = null; 2490 gssapiAuthzID = null; 2491 gssapiQoP = "auth"; 2492 2493 if (bindPassword == null) 2494 { 2495 gssapiAuthPW = null; 2496 } 2497 else 2498 { 2499 gssapiAuthPW = bindPassword.toString().toCharArray(); 2500 } 2501 2502 2503 // Evaluate the properties provided. The authID is required. The authzID, 2504 // KDC, QoP, and realm are optional. 2505 if (saslProperties == null || saslProperties.isEmpty()) 2506 { 2507 LocalizableMessage message = 2508 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_GSSAPI); 2509 throw new ClientException( 2510 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2511 } 2512 2513 for (String name : saslProperties.keySet()) 2514 { 2515 String lowerName = toLowerCase(name); 2516 2517 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 2518 { 2519 List<String> values = saslProperties.get(name); 2520 Iterator<String> iterator = values.iterator(); 2521 if (iterator.hasNext()) 2522 { 2523 gssapiAuthID = iterator.next(); 2524 2525 if (iterator.hasNext()) 2526 { 2527 LocalizableMessage message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get(); 2528 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2529 } 2530 } 2531 } 2532 else if (lowerName.equals(SASL_PROPERTY_AUTHZID)) 2533 { 2534 List<String> values = saslProperties.get(name); 2535 Iterator<String> iterator = values.iterator(); 2536 if (iterator.hasNext()) 2537 { 2538 gssapiAuthzID = iterator.next(); 2539 2540 if (iterator.hasNext()) 2541 { 2542 LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get(); 2543 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 2544 message); 2545 } 2546 } 2547 } 2548 else if (lowerName.equals(SASL_PROPERTY_KDC)) 2549 { 2550 List<String> values = saslProperties.get(name); 2551 Iterator<String> iterator = values.iterator(); 2552 if (iterator.hasNext()) 2553 { 2554 kdc = iterator.next(); 2555 2556 if (iterator.hasNext()) 2557 { 2558 LocalizableMessage message = ERR_LDAPAUTH_KDC_SINGLE_VALUED.get(); 2559 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 2560 message); 2561 } 2562 } 2563 } 2564 else if (lowerName.equals(SASL_PROPERTY_QOP)) 2565 { 2566 List<String> values = saslProperties.get(name); 2567 Iterator<String> iterator = values.iterator(); 2568 if (iterator.hasNext()) 2569 { 2570 gssapiQoP = toLowerCase(iterator.next()); 2571 2572 if (iterator.hasNext()) 2573 { 2574 LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get(); 2575 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 2576 message); 2577 } 2578 2579 if (gssapiQoP.equals("auth")) 2580 { 2581 // This is always fine. 2582 } 2583 else if (gssapiQoP.equals("auth-int") || 2584 gssapiQoP.equals("auth-conf")) 2585 { 2586 // FIXME -- Add support for integrity and confidentiality. 2587 LocalizableMessage message = 2588 ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(gssapiQoP); 2589 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 2590 message); 2591 } 2592 else 2593 { 2594 // This is an illegal value. 2595 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_INVALID_QOP.get(gssapiQoP); 2596 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 2597 message); 2598 } 2599 } 2600 } 2601 else if (lowerName.equals(SASL_PROPERTY_REALM)) 2602 { 2603 List<String> values = saslProperties.get(name); 2604 Iterator<String> iterator = values.iterator(); 2605 if (iterator.hasNext()) 2606 { 2607 realm = iterator.next(); 2608 2609 if (iterator.hasNext()) 2610 { 2611 LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get(); 2612 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 2613 message); 2614 } 2615 } 2616 } 2617 else 2618 { 2619 LocalizableMessage message = 2620 ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_GSSAPI); 2621 throw new ClientException( 2622 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2623 } 2624 } 2625 2626 2627 // Make sure that the authID was provided. 2628 if (gssapiAuthID == null || gssapiAuthID.length() == 0) 2629 { 2630 LocalizableMessage message = 2631 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_GSSAPI); 2632 throw new ClientException( 2633 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2634 } 2635 2636 2637 // See if an authzID was provided. If not, then use the authID. 2638 if (gssapiAuthzID == null) 2639 { 2640 gssapiAuthzID = gssapiAuthID; 2641 } 2642 2643 2644 // See if the realm and/or KDC were specified. If so, then set properties 2645 // that will allow them to be used. Otherwise, we'll hope that the 2646 // underlying system has a valid Kerberos client configuration. 2647 if (realm != null) 2648 { 2649 System.setProperty(KRBV_PROPERTY_REALM, realm); 2650 } 2651 2652 if (kdc != null) 2653 { 2654 System.setProperty(KRBV_PROPERTY_KDC, kdc); 2655 } 2656 2657 2658 // Since we're going to be using JAAS behind the scenes, we need to have a 2659 // JAAS configuration. Rather than always requiring the user to provide it, 2660 // we'll write one to a temporary file that will be deleted when the JVM 2661 // exits. 2662 String configFileName; 2663 try 2664 { 2665 File tempFile = File.createTempFile("login", "conf"); 2666 configFileName = tempFile.getAbsolutePath(); 2667 tempFile.deleteOnExit(); 2668 BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false)); 2669 2670 w.write(getClass().getName() + " {"); 2671 w.newLine(); 2672 2673 w.write(" com.sun.security.auth.module.Krb5LoginModule required " + 2674 "client=TRUE useTicketCache=TRUE;"); 2675 w.newLine(); 2676 2677 w.write("};"); 2678 w.newLine(); 2679 2680 w.flush(); 2681 w.close(); 2682 } 2683 catch (Exception e) 2684 { 2685 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get( 2686 getExceptionMessage(e)); 2687 throw new ClientException( 2688 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2689 } 2690 2691 System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName); 2692 System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "true"); 2693 2694 2695 // The rest of this code must be executed via JAAS, so it will have to go 2696 // in the "run" method. 2697 LoginContext loginContext; 2698 try 2699 { 2700 loginContext = new LoginContext(getClass().getName(), this); 2701 loginContext.login(); 2702 } 2703 catch (Exception e) 2704 { 2705 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED.get( 2706 getExceptionMessage(e)); 2707 throw new ClientException( 2708 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2709 } 2710 2711 try 2712 { 2713 Subject.doAs(loginContext.getSubject(), this); 2714 } 2715 catch (Exception e) 2716 { 2717 if (e instanceof ClientException) 2718 { 2719 throw (ClientException) e; 2720 } 2721 else if (e instanceof LDAPException) 2722 { 2723 throw (LDAPException) e; 2724 } 2725 2726 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_REMOTE_AUTHENTICATION_FAILED.get( 2727 getExceptionMessage(e)); 2728 throw new ClientException( 2729 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2730 } 2731 2732 2733 // FIXME -- Need to make sure we handle request and response controls 2734 // properly, and also check for any possible message to send back to the 2735 // client. 2736 return null; 2737 } 2738 2739 2740 2741 /** 2742 * Retrieves the set of properties that a client may provide when performing a 2743 * SASL EXTERNAL bind, mapped from the property names to their corresponding 2744 * descriptions. 2745 * 2746 * @return The set of properties that a client may provide when performing a 2747 * SASL EXTERNAL bind, mapped from the property names to their 2748 * corresponding descriptions. 2749 */ 2750 public static LinkedHashMap<String,LocalizableMessage> getSASLGSSAPIProperties() 2751 { 2752 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(4); 2753 2754 properties.put(SASL_PROPERTY_AUTHID, 2755 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 2756 properties.put(SASL_PROPERTY_AUTHZID, 2757 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get()); 2758 properties.put(SASL_PROPERTY_KDC, 2759 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_KDC.get()); 2760 properties.put(SASL_PROPERTY_REALM, 2761 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get()); 2762 2763 return properties; 2764 } 2765 2766 2767 2768 /** 2769 * Processes a SASL PLAIN bind with the provided information. 2770 * 2771 * @param bindDN The DN to use to bind to the Directory Server, or 2772 * <CODE>null</CODE> if the authentication identity 2773 * is to be set through some other means. 2774 * @param bindPassword The password to use to bind to the Directory 2775 * Server. 2776 * @param saslProperties A set of additional properties that may be needed 2777 * to process the SASL bind. 2778 * @param requestControls The set of controls to include the request to the 2779 * server. 2780 * @param responseControls A list to hold the set of controls included in 2781 * the response from the server. 2782 * 2783 * @return A message providing additional information about the bind if 2784 * appropriate, or <CODE>null</CODE> if there is no special 2785 * information available. 2786 * 2787 * @throws ClientException If a client-side problem prevents the bind 2788 * attempt from succeeding. 2789 * 2790 * @throws LDAPException If the bind fails or some other server-side problem 2791 * occurs during processing. 2792 */ 2793 public String doSASLPlain(ByteSequence bindDN, 2794 ByteSequence bindPassword, 2795 Map<String,List<String>> saslProperties, 2796 List<Control> requestControls, 2797 List<Control> responseControls) 2798 throws ClientException, LDAPException 2799 { 2800 String authID = null; 2801 String authzID = null; 2802 2803 2804 // Evaluate the properties provided. The authID is required, and authzID is 2805 // optional. 2806 if (saslProperties == null || saslProperties.isEmpty()) 2807 { 2808 LocalizableMessage message = 2809 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_PLAIN); 2810 throw new ClientException( 2811 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2812 } 2813 2814 for (String name : saslProperties.keySet()) 2815 { 2816 String lowerName = toLowerCase(name); 2817 2818 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 2819 { 2820 authID = getAuthID(saslProperties, authID, name); 2821 } 2822 else if (lowerName.equals(SASL_PROPERTY_AUTHZID)) 2823 { 2824 List<String> values = saslProperties.get(name); 2825 Iterator<String> iterator = values.iterator(); 2826 if (iterator.hasNext()) 2827 { 2828 authzID = iterator.next(); 2829 2830 if (iterator.hasNext()) 2831 { 2832 LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get(); 2833 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 2834 message); 2835 } 2836 } 2837 } 2838 else 2839 { 2840 LocalizableMessage message = 2841 ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_PLAIN); 2842 throw new ClientException( 2843 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2844 } 2845 } 2846 2847 2848 // Make sure that at least the authID was provided. 2849 if (authID == null || authID.length() == 0) 2850 { 2851 LocalizableMessage message = 2852 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_PLAIN); 2853 throw new ClientException( 2854 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2855 } 2856 2857 2858 // Set password to ByteString.empty if the password is null. 2859 if (bindPassword == null) 2860 { 2861 bindPassword = ByteString.empty(); 2862 } 2863 2864 2865 // Construct the bind request and send it to the server. 2866 StringBuilder credBuffer = new StringBuilder(); 2867 if (authzID != null) 2868 { 2869 credBuffer.append(authzID); 2870 } 2871 credBuffer.append('\u0000'); 2872 credBuffer.append(authID); 2873 credBuffer.append('\u0000'); 2874 credBuffer.append(bindPassword.toString()); 2875 2876 ByteString saslCredentials = 2877 ByteString.valueOfUtf8(credBuffer.toString()); 2878 BindRequestProtocolOp bindRequest = 2879 new BindRequestProtocolOp(bindDN.toByteString(), SASL_MECHANISM_PLAIN, 2880 saslCredentials); 2881 LDAPMessage requestMessage = 2882 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, 2883 requestControls); 2884 2885 try 2886 { 2887 writer.writeMessage(requestMessage); 2888 } 2889 catch (IOException ioe) 2890 { 2891 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 2892 SASL_MECHANISM_PLAIN, getExceptionMessage(ioe)); 2893 throw new ClientException( 2894 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 2895 } 2896 catch (Exception e) 2897 { 2898 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 2899 SASL_MECHANISM_PLAIN, getExceptionMessage(e)); 2900 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 2901 message, e); 2902 } 2903 2904 2905 // Read the response from the server. 2906 LDAPMessage responseMessage; 2907 try 2908 { 2909 responseMessage = reader.readMessage(); 2910 if (responseMessage == null) 2911 { 2912 LocalizableMessage message = 2913 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 2914 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 2915 message); 2916 } 2917 } 2918 catch (DecodeException | LDAPException e) 2919 { 2920 LocalizableMessage message = 2921 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 2922 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 2923 } 2924 catch (IOException ioe) 2925 { 2926 LocalizableMessage message = 2927 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe)); 2928 throw new ClientException( 2929 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 2930 } 2931 catch (Exception e) 2932 { 2933 LocalizableMessage message = 2934 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 2935 throw new ClientException( 2936 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2937 } 2938 2939 2940 // See if there are any controls in the response. If so, then add them to 2941 // the response controls list. 2942 List<Control> respControls = responseMessage.getControls(); 2943 if (respControls != null && !respControls.isEmpty()) 2944 { 2945 responseControls.addAll(respControls); 2946 } 2947 2948 2949 // Look at the protocol op from the response. If it's a bind response, then 2950 // continue. If it's an extended response, then it could be a notice of 2951 // disconnection so check for that. Otherwise, generate an error. 2952 generateError(responseMessage); 2953 2954 2955 BindResponseProtocolOp bindResponse = 2956 responseMessage.getBindResponseProtocolOp(); 2957 int resultCode = bindResponse.getResultCode(); 2958 if (resultCode == ReturnCode.SUCCESS.get()) 2959 { 2960 // FIXME -- Need to look for things like password expiration warning, 2961 // reset notice, etc. 2962 return null; 2963 } 2964 2965 // FIXME -- Add support for referrals. 2966 2967 LocalizableMessage message = ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_PLAIN); 2968 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), 2969 message, bindResponse.getMatchedDN(), null); 2970 } 2971 2972 2973 2974 /** 2975 * Retrieves the set of properties that a client may provide when performing a 2976 * SASL PLAIN bind, mapped from the property names to their corresponding 2977 * descriptions. 2978 * 2979 * @return The set of properties that a client may provide when performing a 2980 * SASL PLAIN bind, mapped from the property names to their 2981 * corresponding descriptions. 2982 */ 2983 public static LinkedHashMap<String,LocalizableMessage> getSASLPlainProperties() 2984 { 2985 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(2); 2986 2987 properties.put(SASL_PROPERTY_AUTHID, 2988 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 2989 properties.put(SASL_PROPERTY_AUTHZID, 2990 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get()); 2991 2992 return properties; 2993 } 2994 2995 2996 2997 /** 2998 * Performs a privileged operation under JAAS so that the local authentication 2999 * information can be available for the SASL bind to the Directory Server. 3000 * 3001 * @return A placeholder object in order to comply with the 3002 * <CODE>PrivilegedExceptionAction</CODE> interface. 3003 * 3004 * @throws ClientException If a client-side problem occurs during the bind 3005 * processing. 3006 * 3007 * @throws LDAPException If a server-side problem occurs during the bind 3008 * processing. 3009 */ 3010 @Override 3011 public Object run() 3012 throws ClientException, LDAPException 3013 { 3014 if (saslMechanism == null) 3015 { 3016 LocalizableMessage message = ERR_LDAPAUTH_NONSASL_RUN_INVOCATION.get(getBacktrace()); 3017 throw new ClientException( 3018 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 3019 } 3020 else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI)) 3021 { 3022 // Create the property map that will be used by the internal SASL handler. 3023 HashMap<String,String> saslProperties = new HashMap<>(); 3024 saslProperties.put(Sasl.QOP, gssapiQoP); 3025 saslProperties.put(Sasl.SERVER_AUTH, "true"); 3026 3027 3028 // Create the SASL client that we will use to actually perform the 3029 // authentication. 3030 SaslClient saslClient; 3031 try 3032 { 3033 saslClient = 3034 Sasl.createSaslClient(new String[] { SASL_MECHANISM_GSSAPI }, 3035 gssapiAuthzID, "ldap", hostName, 3036 saslProperties, this); 3037 } 3038 catch (Exception e) 3039 { 3040 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get( 3041 getExceptionMessage(e)); 3042 throw new ClientException( 3043 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 3044 } 3045 3046 3047 // Get the SASL credentials to include in the initial bind request. 3048 ByteString saslCredentials; 3049 if (saslClient.hasInitialResponse()) 3050 { 3051 try 3052 { 3053 byte[] credBytes = saslClient.evaluateChallenge(new byte[0]); 3054 saslCredentials = ByteString.wrap(credBytes); 3055 } 3056 catch (Exception e) 3057 { 3058 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_INITIAL_CHALLENGE. 3059 get(getExceptionMessage(e)); 3060 throw new ClientException( 3061 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 3062 message, e); 3063 } 3064 } 3065 else 3066 { 3067 saslCredentials = null; 3068 } 3069 3070 3071 BindRequestProtocolOp bindRequest = 3072 new BindRequestProtocolOp(gssapiBindDN.toByteString(), 3073 SASL_MECHANISM_GSSAPI, saslCredentials); 3074 // FIXME -- Add controls here? 3075 LDAPMessage requestMessage = 3076 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest); 3077 3078 try 3079 { 3080 writer.writeMessage(requestMessage); 3081 } 3082 catch (IOException ioe) 3083 { 3084 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 3085 SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe)); 3086 throw new ClientException( 3087 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 3088 } 3089 catch (Exception e) 3090 { 3091 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 3092 SASL_MECHANISM_GSSAPI, getExceptionMessage(e)); 3093 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 3094 message, e); 3095 } 3096 3097 3098 // Read the response from the server. 3099 LDAPMessage responseMessage; 3100 try 3101 { 3102 responseMessage = reader.readMessage(); 3103 if (responseMessage == null) 3104 { 3105 LocalizableMessage message = 3106 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 3107 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 3108 message); 3109 } 3110 } 3111 catch (DecodeException | LDAPException e) 3112 { 3113 LocalizableMessage message = 3114 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 3115 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 3116 } 3117 catch (IOException ioe) 3118 { 3119 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get( 3120 getExceptionMessage(ioe)); 3121 throw new ClientException( 3122 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 3123 } 3124 catch (Exception e) 3125 { 3126 LocalizableMessage message = 3127 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 3128 throw new ClientException( 3129 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 3130 } 3131 3132 3133 // FIXME -- Handle response controls. 3134 3135 3136 // Look at the protocol op from the response. If it's a bind response, 3137 // then continue. If it's an extended response, then it could be a notice 3138 // of disconnection so check for that. Otherwise, generate an error. 3139 generateError(responseMessage); 3140 3141 3142 while (true) 3143 { 3144 BindResponseProtocolOp bindResponse = 3145 responseMessage.getBindResponseProtocolOp(); 3146 int resultCode = bindResponse.getResultCode(); 3147 if (resultCode == ReturnCode.SUCCESS.get()) 3148 { 3149 // We should be done after this, but we still need to look for and 3150 // handle the server SASL credentials. 3151 ByteString serverSASLCredentials = 3152 bindResponse.getServerSASLCredentials(); 3153 if (serverSASLCredentials != null) 3154 { 3155 try 3156 { 3157 saslClient.evaluateChallenge(serverSASLCredentials.toByteArray()); 3158 } 3159 catch (Exception e) 3160 { 3161 LocalizableMessage message = 3162 ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS. 3163 get(getExceptionMessage(e)); 3164 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 3165 message, e); 3166 } 3167 } 3168 3169 3170 // Just to be sure, check that the login really is complete. 3171 if (! saslClient.isComplete()) 3172 { 3173 LocalizableMessage message = 3174 ERR_LDAPAUTH_GSSAPI_UNEXPECTED_SUCCESS_RESPONSE.get(); 3175 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 3176 message); 3177 } 3178 3179 break; 3180 } 3181 else if (resultCode == ReturnCode.SASL_BIND_IN_PROGRESS.get()) 3182 { 3183 // Read the response and process the server SASL credentials. 3184 ByteString serverSASLCredentials = 3185 bindResponse.getServerSASLCredentials(); 3186 byte[] credBytes; 3187 try 3188 { 3189 if (serverSASLCredentials == null) 3190 { 3191 credBytes = saslClient.evaluateChallenge(new byte[0]); 3192 } 3193 else 3194 { 3195 credBytes = saslClient.evaluateChallenge( 3196 serverSASLCredentials.toByteArray()); 3197 } 3198 } 3199 catch (Exception e) 3200 { 3201 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS. 3202 get(getExceptionMessage(e)); 3203 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 3204 message, e); 3205 } 3206 3207 3208 // Send the next bind in the sequence to the server. 3209 bindRequest = 3210 new BindRequestProtocolOp(gssapiBindDN.toByteString(), 3211 SASL_MECHANISM_GSSAPI, ByteString.wrap(credBytes)); 3212 // FIXME -- Add controls here? 3213 requestMessage = 3214 new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest); 3215 3216 3217 try 3218 { 3219 writer.writeMessage(requestMessage); 3220 } 3221 catch (IOException ioe) 3222 { 3223 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 3224 SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe)); 3225 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 3226 message, ioe); 3227 } 3228 catch (Exception e) 3229 { 3230 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get( 3231 SASL_MECHANISM_GSSAPI, getExceptionMessage(e)); 3232 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 3233 message, e); 3234 } 3235 3236 3237 // Read the response from the server. 3238 try 3239 { 3240 responseMessage = reader.readMessage(); 3241 if (responseMessage == null) 3242 { 3243 LocalizableMessage message = 3244 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 3245 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 3246 message); 3247 } 3248 } 3249 catch (DecodeException | LDAPException e) 3250 { 3251 LocalizableMessage message = 3252 ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e)); 3253 throw new ClientException( 3254 ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 3255 } 3256 catch (IOException ioe) 3257 { 3258 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get( 3259 getExceptionMessage(ioe)); 3260 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 3261 message, ioe); 3262 } 3263 catch (Exception e) 3264 { 3265 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get( 3266 getExceptionMessage(e)); 3267 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 3268 message, e); 3269 } 3270 3271 3272 // FIXME -- Handle response controls. 3273 3274 3275 // Look at the protocol op from the response. If it's a bind 3276 // response, then continue. If it's an extended response, then it 3277 // could be a notice of disconnection so check for that. Otherwise, 3278 // generate an error. 3279 generateError(responseMessage); 3280 } 3281 else 3282 { 3283 // This is an error. 3284 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get(); 3285 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), 3286 message, bindResponse.getMatchedDN(), 3287 null); 3288 } 3289 } 3290 } 3291 else 3292 { 3293 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RUN_INVOCATION.get( 3294 saslMechanism, getBacktrace()); 3295 throw new ClientException( 3296 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 3297 } 3298 3299 3300 // FIXME -- Need to look for things like password expiration warning, reset 3301 // notice, etc. 3302 return null; 3303 } 3304 3305 private void generateError(LDAPMessage responseMessage) throws LDAPException, ClientException 3306 { 3307 switch (responseMessage.getProtocolOpType()) 3308 { 3309 case OP_TYPE_BIND_RESPONSE: 3310 // We'll deal with this later. 3311 break; 3312 3313 case OP_TYPE_EXTENDED_RESPONSE: 3314 ExtendedResponseProtocolOp extendedResponse = 3315 responseMessage.getExtendedResponseProtocolOp(); 3316 String responseOID = extendedResponse.getOID(); 3317 if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID)) 3318 { 3319 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT. 3320 get(extendedResponse.getResultCode(), extendedResponse.getErrorMessage()); 3321 throw new LDAPException(extendedResponse.getResultCode(), message); 3322 } 3323 else 3324 { 3325 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse); 3326 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 3327 } 3328 3329 default: 3330 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp()); 3331 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 3332 } 3333 } 3334 3335 /** 3336 * Handles the authentication callbacks to provide information needed by the 3337 * JAAS login process. 3338 * 3339 * @param callbacks The callbacks needed to provide information for the JAAS 3340 * login process. 3341 * 3342 * @throws UnsupportedCallbackException If an unexpected callback is 3343 * included in the provided set. 3344 */ 3345 @Override 3346 public void handle(Callback[] callbacks) 3347 throws UnsupportedCallbackException 3348 { 3349 if (saslMechanism == null) 3350 { 3351 LocalizableMessage message = 3352 ERR_LDAPAUTH_NONSASL_CALLBACK_INVOCATION.get(getBacktrace()); 3353 throw new UnsupportedCallbackException(callbacks[0], message.toString()); 3354 } 3355 else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI)) 3356 { 3357 for (Callback cb : callbacks) 3358 { 3359 if (cb instanceof NameCallback) 3360 { 3361 ((NameCallback) cb).setName(gssapiAuthID); 3362 } 3363 else if (cb instanceof PasswordCallback) 3364 { 3365 if (gssapiAuthPW == null) 3366 { 3367 System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(gssapiAuthID)); 3368 try 3369 { 3370 gssapiAuthPW = ConsoleApplication.readPassword(); 3371 } 3372 catch (ClientException e) 3373 { 3374 throw new UnsupportedCallbackException(cb, e.getLocalizedMessage()); 3375 } 3376 } 3377 3378 ((PasswordCallback) cb).setPassword(gssapiAuthPW); 3379 } 3380 else 3381 { 3382 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_GSSAPI_CALLBACK.get(cb); 3383 throw new UnsupportedCallbackException(cb, message.toString()); 3384 } 3385 } 3386 } 3387 else 3388 { 3389 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_CALLBACK_INVOCATION.get( 3390 saslMechanism, getBacktrace()); 3391 throw new UnsupportedCallbackException(callbacks[0], message.toString()); 3392 } 3393 } 3394 3395 3396 3397 /** 3398 * Uses the "Who Am I?" extended operation to request that the server provide 3399 * the client with the authorization identity for this connection. 3400 * 3401 * @return An ASN.1 octet string containing the authorization identity, or 3402 * <CODE>null</CODE> if the client is not authenticated or is 3403 * authenticated anonymously. 3404 * 3405 * @throws ClientException If a client-side problem occurs during the 3406 * request processing. 3407 * 3408 * @throws LDAPException If a server-side problem occurs during the request 3409 * processing. 3410 */ 3411 public ByteString requestAuthorizationIdentity() 3412 throws ClientException, LDAPException 3413 { 3414 // Construct the extended request and send it to the server. 3415 ExtendedRequestProtocolOp extendedRequest = 3416 new ExtendedRequestProtocolOp(OID_WHO_AM_I_REQUEST); 3417 LDAPMessage requestMessage = 3418 new LDAPMessage(nextMessageID.getAndIncrement(), extendedRequest); 3419 3420 try 3421 { 3422 writer.writeMessage(requestMessage); 3423 } 3424 catch (IOException ioe) 3425 { 3426 LocalizableMessage message = 3427 ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(ioe)); 3428 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 3429 message, ioe); 3430 } 3431 catch (Exception e) 3432 { 3433 LocalizableMessage message = 3434 ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(e)); 3435 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 3436 message, e); 3437 } 3438 3439 3440 // Read the response from the server. 3441 LDAPMessage responseMessage; 3442 try 3443 { 3444 responseMessage = reader.readMessage(); 3445 if (responseMessage == null) 3446 { 3447 LocalizableMessage message = 3448 ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 3449 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 3450 message); 3451 } 3452 } 3453 catch (DecodeException | LDAPException e) 3454 { 3455 LocalizableMessage message = 3456 ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e)); 3457 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 3458 } 3459 catch (IOException ioe) 3460 { 3461 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get( 3462 getExceptionMessage(ioe)); 3463 throw new ClientException( 3464 ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 3465 } 3466 catch (Exception e) 3467 { 3468 LocalizableMessage message = 3469 ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e)); 3470 throw new ClientException( 3471 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 3472 } 3473 3474 3475 // If the protocol op isn't an extended response, then that's a problem. 3476 if (responseMessage.getProtocolOpType() != OP_TYPE_EXTENDED_RESPONSE) 3477 { 3478 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp()); 3479 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 3480 } 3481 3482 3483 // Get the extended response and see if it has the "notice of disconnection" 3484 // OID. If so, then the server is closing the connection. 3485 ExtendedResponseProtocolOp extendedResponse = 3486 responseMessage.getExtendedResponseProtocolOp(); 3487 String responseOID = extendedResponse.getOID(); 3488 if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID)) 3489 { 3490 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.get( 3491 extendedResponse.getResultCode(), extendedResponse.getErrorMessage()); 3492 throw new LDAPException(extendedResponse.getResultCode(), message); 3493 } 3494 3495 3496 // It isn't a notice of disconnection so it must be the "Who Am I?" 3497 // response and the value would be the authorization ID. However, first 3498 // check that it was successful. If it was not, then fail. 3499 int resultCode = extendedResponse.getResultCode(); 3500 if (resultCode != ReturnCode.SUCCESS.get()) 3501 { 3502 LocalizableMessage message = ERR_LDAPAUTH_WHOAMI_FAILED.get(); 3503 throw new LDAPException(resultCode, extendedResponse.getErrorMessage(), 3504 message, extendedResponse.getMatchedDN(), 3505 null); 3506 } 3507 3508 3509 // Get the authorization ID (if there is one) and return it to the caller. 3510 ByteString authzID = extendedResponse.getValue(); 3511 if (authzID == null || authzID.length() == 0) 3512 { 3513 return null; 3514 } 3515 3516 String valueString = authzID.toString(); 3517 if (valueString == null || valueString.length() == 0 || 3518 valueString.equalsIgnoreCase("dn:")) 3519 { 3520 return null; 3521 } 3522 3523 return authzID; 3524 } 3525}