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