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