001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2006-2009 Sun Microsystems, Inc. 015 * Portions Copyright 2012-2016 ForgeRock AS. 016 */ 017package org.opends.server.tools; 018 019import static com.forgerock.opendj.cli.ArgumentConstants.*; 020 021import static org.opends.messages.ToolMessages.*; 022import static org.opends.server.protocols.ldap.LDAPConstants.*; 023import static org.opends.server.util.ServerConstants.*; 024import static org.opends.server.util.StaticUtils.*; 025 026import java.io.BufferedWriter; 027import java.io.File; 028import java.io.FileWriter; 029import java.io.IOException; 030import java.io.UnsupportedEncodingException; 031import java.security.MessageDigest; 032import java.security.PrivilegedExceptionAction; 033import java.security.SecureRandom; 034import java.util.Arrays; 035import java.util.HashMap; 036import java.util.Iterator; 037import java.util.LinkedHashMap; 038import java.util.LinkedList; 039import java.util.List; 040import java.util.Map; 041import java.util.Map.Entry; 042import java.util.StringTokenizer; 043import java.util.concurrent.atomic.AtomicInteger; 044 045import javax.security.auth.Subject; 046import javax.security.auth.callback.Callback; 047import javax.security.auth.callback.CallbackHandler; 048import javax.security.auth.callback.NameCallback; 049import javax.security.auth.callback.PasswordCallback; 050import javax.security.auth.callback.UnsupportedCallbackException; 051import javax.security.auth.login.LoginContext; 052import javax.security.sasl.Sasl; 053import javax.security.sasl.SaslClient; 054 055import org.forgerock.i18n.LocalizableMessage; 056import org.forgerock.i18n.LocalizableMessageDescriptor.Arg0; 057import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1; 058import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2; 059import org.forgerock.opendj.ldap.ByteSequence; 060import org.forgerock.opendj.ldap.ByteString; 061import org.forgerock.opendj.ldap.DecodeException; 062import org.opends.server.protocols.ldap.BindRequestProtocolOp; 063import org.opends.server.protocols.ldap.BindResponseProtocolOp; 064import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp; 065import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp; 066import org.opends.server.protocols.ldap.LDAPMessage; 067import org.opends.server.types.Control; 068import org.opends.server.types.LDAPException; 069import org.opends.server.util.Base64; 070 071import com.forgerock.opendj.cli.ClientException; 072import com.forgerock.opendj.cli.ConsoleApplication; 073import com.forgerock.opendj.cli.ReturnCode; 074 075/** 076 * This class provides a generic interface that LDAP clients can use to perform 077 * various kinds of authentication to the Directory Server. This handles both 078 * simple authentication as well as several SASL mechanisms including: 079 * <UL> 080 * <LI>ANONYMOUS</LI> 081 * <LI>CRAM-MD5</LI> 082 * <LI>DIGEST-MD5</LI> 083 * <LI>EXTERNAL</LI> 084 * <LI>GSSAPI</LI> 085 * <LI>PLAIN</LI> 086 * </UL> 087 * <BR><BR> 088 * Note that this implementation is not thread safe, so if the same 089 * <CODE>AuthenticationHandler</CODE> object is to be used concurrently by 090 * multiple threads, it must be externally synchronized. 091 */ 092public class LDAPAuthenticationHandler 093 implements PrivilegedExceptionAction<Object>, CallbackHandler 094{ 095 /** The LDAP reader that will be used to read data from the server. */ 096 private final LDAPReader reader; 097 /** The LDAP writer that will be used to send data to the server. */ 098 private final LDAPWriter writer; 099 100 /** The atomic integer that will be used to obtain message IDs for request messages. */ 101 private final AtomicInteger nextMessageID; 102 103 /** An array filled with the inner pad byte. */ 104 private byte[] iPad; 105 /** An array filled with the outer pad byte. */ 106 private byte[] oPad; 107 108 /** The message digest that will be used to create MD5 hashes. */ 109 private MessageDigest md5Digest; 110 /** The secure random number generator for use by this authentication handler. */ 111 private SecureRandom secureRandom; 112 113 /** The bind DN for GSSAPI authentication. */ 114 private ByteSequence gssapiBindDN; 115 /** The authentication ID for GSSAPI authentication. */ 116 private String gssapiAuthID; 117 /** The authorization ID for GSSAPI authentication. */ 118 private String gssapiAuthzID; 119 /** The authentication password for GSSAPI authentication. */ 120 private char[] gssapiAuthPW; 121 /** The quality of protection for GSSAPI authentication. */ 122 private String gssapiQoP; 123 124 /** The host name used to connect to the remote system. */ 125 private final String hostName; 126 127 /** The SASL mechanism that will be used for callback authentication. */ 128 private String saslMechanism; 129 130 131 132 /** 133 * Creates a new instance of this authentication handler. All initialization 134 * will be done lazily to avoid unnecessary performance hits, particularly 135 * for cases in which simple authentication will be used as it does not 136 * require any particularly expensive processing. 137 * 138 * @param reader The LDAP reader that will be used to read data from 139 * the server. 140 * @param writer The LDAP writer that will be used to send data to 141 * the server. 142 * @param hostName The host name used to connect to the remote system 143 * (fully-qualified if possible). 144 * @param nextMessageID The atomic integer that will be used to obtain 145 * message IDs for request messages. 146 */ 147 public LDAPAuthenticationHandler(LDAPReader reader, LDAPWriter writer, 148 String hostName, AtomicInteger nextMessageID) 149 { 150 this.reader = reader; 151 this.writer = writer; 152 this.hostName = hostName; 153 this.nextMessageID = nextMessageID; 154 155 md5Digest = null; 156 secureRandom = null; 157 iPad = null; 158 oPad = null; 159 } 160 161 162 163 /** 164 * Retrieves a list of the SASL mechanisms that are supported by this client 165 * library. 166 * 167 * @return A list of the SASL mechanisms that are supported by this client 168 * library. 169 */ 170 public static String[] getSupportedSASLMechanisms() 171 { 172 return new String[] 173 { 174 SASL_MECHANISM_ANONYMOUS, 175 SASL_MECHANISM_CRAM_MD5, 176 SASL_MECHANISM_DIGEST_MD5, 177 SASL_MECHANISM_EXTERNAL, 178 SASL_MECHANISM_GSSAPI, 179 SASL_MECHANISM_PLAIN 180 }; 181 } 182 183 184 185 /** 186 * Retrieves a list of the SASL properties that may be provided for the 187 * specified SASL mechanism, mapped from the property names to their 188 * corresponding descriptions. 189 * 190 * @param mechanism The name of the SASL mechanism for which to obtain the 191 * list of supported properties. 192 * 193 * @return A list of the SASL properties that may be provided for the 194 * specified SASL mechanism, mapped from the property names to their 195 * corresponding descriptions. 196 */ 197 public static Map<String, LocalizableMessage> getSASLProperties(String mechanism) 198 { 199 switch (toUpperCase(mechanism)) 200 { 201 case SASL_MECHANISM_ANONYMOUS: 202 return getSASLAnonymousProperties(); 203 case SASL_MECHANISM_CRAM_MD5: 204 return getSASLCRAMMD5Properties(); 205 case SASL_MECHANISM_DIGEST_MD5: 206 return getSASLDigestMD5Properties(); 207 case SASL_MECHANISM_EXTERNAL: 208 return getSASLExternalProperties(); 209 case SASL_MECHANISM_GSSAPI: 210 return getSASLGSSAPIProperties(); 211 case SASL_MECHANISM_PLAIN: 212 return getSASLPlainProperties(); 213 default: 214 // This is an unsupported mechanism. 215 return null; 216 } 217 } 218 219 220 221 /** 222 * Processes a bind using simple authentication with the provided information. 223 * If the bind fails, then an exception will be thrown with information about 224 * the reason for the failure. If the bind is successful but there may be 225 * some special information that the client should be given, then it will be 226 * returned as a String. 227 * 228 * @param ldapVersion The LDAP protocol version to use for the bind 229 * request. 230 * @param bindDN The DN to use to bind to the Directory Server, or 231 * <CODE>null</CODE> if it is to be an anonymous 232 * bind. 233 * @param bindPassword The password to use to bind to the Directory 234 * Server, or <CODE>null</CODE> if it is to be an 235 * anonymous bind. 236 * @param requestControls The set of controls to include the request to the 237 * server. 238 * @param responseControls A list to hold the set of controls included in 239 * the response from the server. 240 * 241 * @return A message providing additional information about the bind if 242 * appropriate, or <CODE>null</CODE> if there is no special 243 * information available. 244 * 245 * @throws ClientException If a client-side problem prevents the bind 246 * attempt from succeeding. 247 * 248 * @throws LDAPException If the bind fails or some other server-side problem 249 * occurs during processing. 250 */ 251 public String doSimpleBind(int ldapVersion, ByteSequence bindDN, 252 ByteSequence bindPassword, 253 List<Control> requestControls, 254 List<Control> responseControls) 255 throws ClientException, LDAPException 256 { 257 //Password is empty, set it to ByteString.empty. 258 if (bindPassword == null) 259 { 260 bindPassword = ByteString.empty(); 261 } 262 263 // Make sure that critical elements aren't null. 264 if (bindDN == null) 265 { 266 bindDN = ByteString.empty(); 267 } 268 269 sendSimpleBindRequest(ldapVersion, bindDN, bindPassword, requestControls); 270 271 LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE); 272 responseControls.addAll(responseMessage.getControls()); 273 checkConnected(responseMessage); 274 return checkSuccessfulSimpleBind(responseMessage); 275 } 276 277 private void sendSimpleBindRequest(int ldapVersion, ByteSequence bindDN, ByteSequence bindPassword, 278 List<Control> requestControls) throws ClientException 279 { 280 BindRequestProtocolOp bindRequest = 281 new BindRequestProtocolOp(bindDN.toByteString(), ldapVersion, bindPassword.toByteString()); 282 LDAPMessage bindRequestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, requestControls); 283 284 try 285 { 286 writer.writeMessage(bindRequestMessage); 287 } 288 catch (IOException ioe) 289 { 290 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(ioe)); 291 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 292 } 293 catch (Exception e) 294 { 295 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(e)); 296 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e); 297 } 298 } 299 300 private BindResponseProtocolOp checkSuccessfulBind(LDAPMessage responseMessage, String saslMechanism) 301 throws LDAPException 302 { 303 BindResponseProtocolOp bindResponse = responseMessage.getBindResponseProtocolOp(); 304 int resultCode = bindResponse.getResultCode(); 305 if (resultCode != ReturnCode.SUCCESS.get()) 306 { 307 // FIXME -- Add support for referrals. 308 LocalizableMessage message = ERR_LDAPAUTH_SASL_BIND_FAILED.get(saslMechanism); 309 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), message, bindResponse.getMatchedDN(), null); 310 } 311 // FIXME -- Need to look for things like password expiration warning, reset notice, etc. 312 return bindResponse; 313 } 314 315 private String checkSuccessfulSimpleBind(LDAPMessage responseMessage) throws LDAPException 316 { 317 BindResponseProtocolOp bindResponse = responseMessage.getBindResponseProtocolOp(); 318 int resultCode = bindResponse.getResultCode(); 319 if (resultCode != ReturnCode.SUCCESS.get()) 320 { 321 // FIXME -- Add support for referrals. 322 LocalizableMessage message = ERR_LDAPAUTH_SIMPLE_BIND_FAILED.get(); 323 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), message, bindResponse.getMatchedDN(), null); 324 } 325 // FIXME -- Need to look for things like password expiration warning, reset notice, etc. 326 return null; 327 } 328 329 /** 330 * Processes a SASL bind using the provided information. If the bind fails, 331 * then an exception will be thrown with information about the reason for the 332 * failure. If the bind is successful but there may be some special 333 * information that the client should be given, then it will be returned as a 334 * String. 335 * 336 * @param bindDN The DN to use to bind to the Directory Server, or 337 * <CODE>null</CODE> if the authentication identity 338 * is to be set through some other means. 339 * @param bindPassword The password to use to bind to the Directory 340 * Server, or <CODE>null</CODE> if this is not a 341 * password-based SASL mechanism. 342 * @param mechanism The name of the SASL mechanism to use to 343 * authenticate to the Directory Server. 344 * @param saslProperties A set of additional properties that may be needed 345 * to process the SASL bind. 346 * @param requestControls The set of controls to include the request to the 347 * server. 348 * @param responseControls A list to hold the set of controls included in 349 * the response from the server. 350 * 351 * @return A message providing additional information about the bind if 352 * appropriate, or <CODE>null</CODE> if there is no special 353 * information available. 354 * 355 * @throws ClientException If a client-side problem prevents the bind 356 * attempt from succeeding. 357 * 358 * @throws LDAPException If the bind fails or some other server-side problem 359 * occurs during processing. 360 */ 361 public String doSASLBind(ByteSequence bindDN, ByteSequence bindPassword, 362 String mechanism, 363 Map<String,List<String>> saslProperties, 364 List<Control> requestControls, 365 List<Control> responseControls) 366 throws ClientException, LDAPException 367 { 368 // Make sure that critical elements aren't null. 369 if (bindDN == null) 370 { 371 bindDN = ByteString.empty(); 372 } 373 374 if (mechanism == null || mechanism.length() == 0) 375 { 376 LocalizableMessage message = ERR_LDAPAUTH_NO_SASL_MECHANISM.get(); 377 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 378 } 379 380 381 // Look at the mechanism name and call the appropriate method to process the request. 382 saslMechanism = toUpperCase(mechanism); 383 switch (saslMechanism) 384 { 385 case SASL_MECHANISM_ANONYMOUS: 386 return doSASLAnonymous(bindDN, saslProperties, requestControls, responseControls); 387 case SASL_MECHANISM_CRAM_MD5: 388 return doSASLCRAMMD5(bindDN, bindPassword, saslProperties, requestControls, responseControls); 389 case SASL_MECHANISM_DIGEST_MD5: 390 return doSASLDigestMD5(bindDN, bindPassword, saslProperties, requestControls, responseControls); 391 case SASL_MECHANISM_EXTERNAL: 392 return doSASLExternal(bindDN, saslProperties, requestControls, responseControls); 393 case SASL_MECHANISM_GSSAPI: 394 return doSASLGSSAPI(bindDN, bindPassword, saslProperties, requestControls, responseControls); 395 case SASL_MECHANISM_PLAIN: 396 return doSASLPlain(bindDN, bindPassword, saslProperties, requestControls, responseControls); 397 default: 398 LocalizableMessage message = ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mechanism); 399 throw new ClientException(ReturnCode.CLIENT_SIDE_AUTH_UNKNOWN, message); 400 } 401 } 402 403 404 405 /** 406 * Processes a SASL ANONYMOUS bind with the provided information. 407 * 408 * @param bindDN The DN to use to bind to the Directory Server, or 409 * <CODE>null</CODE> if the authentication identity 410 * is to be set through some other means. 411 * @param saslProperties A set of additional properties that may be needed 412 * to process the SASL bind. 413 * @param requestControls The set of controls to include the request to the 414 * server. 415 * @param responseControls A list to hold the set of controls included in 416 * the response from the server. 417 * 418 * @return A message providing additional information about the bind if 419 * appropriate, or <CODE>null</CODE> if there is no special 420 * information available. 421 * 422 * @throws ClientException If a client-side problem prevents the bind 423 * attempt from succeeding. 424 * 425 * @throws LDAPException If the bind fails or some other server-side problem 426 * occurs during processing. 427 */ 428 private String doSASLAnonymous(ByteSequence bindDN, 429 Map<String,List<String>> saslProperties, 430 List<Control> requestControls, 431 List<Control> responseControls) 432 throws ClientException, LDAPException 433 { 434 String trace = null; 435 436 // The only allowed property is the trace property, but it is not required. 437 if (saslProperties != null) 438 { 439 for (Entry<String, List<String>> entry : saslProperties.entrySet()) 440 { 441 String name = entry.getKey(); 442 List<String> values = entry.getValue(); 443 if (name.equalsIgnoreCase(SASL_PROPERTY_TRACE)) 444 { 445 // This is acceptable, and we'll take any single value. 446 trace = getSingleValue(values, ERR_LDAPAUTH_TRACE_SINGLE_VALUED); 447 } 448 else 449 { 450 LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get( 451 name, SASL_MECHANISM_ANONYMOUS); 452 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 453 } 454 } 455 } 456 457 // Construct the bind request and send it to the server. 458 ByteString saslCredentials = trace != null ? ByteString.valueOfUtf8(trace) : null; 459 sendBindRequest(SASL_MECHANISM_ANONYMOUS, bindDN, saslCredentials, requestControls); 460 461 LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE); 462 responseControls.addAll(responseMessage.getControls()); 463 checkConnected(responseMessage); 464 checkSuccessfulBind(responseMessage, SASL_MECHANISM_ANONYMOUS); 465 return null; 466 } 467 468 /** 469 * Retrieves the set of properties that a client may provide when performing a 470 * SASL ANONYMOUS bind, mapped from the property names to their corresponding 471 * descriptions. 472 * 473 * @return The set of properties that a client may provide when performing a 474 * SASL ANONYMOUS bind, mapped from the property names to their 475 * corresponding descriptions. 476 */ 477 private static LinkedHashMap<String, LocalizableMessage> getSASLAnonymousProperties() 478 { 479 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1); 480 481 properties.put(SASL_PROPERTY_TRACE, 482 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_TRACE.get()); 483 484 return properties; 485 } 486 487 488 489 /** 490 * Processes a SASL CRAM-MD5 bind with the provided information. 491 * 492 * @param bindDN The DN to use to bind to the Directory Server, or 493 * <CODE>null</CODE> if the authentication identity 494 * is to be set through some other means. 495 * @param bindPassword The password to use to bind to the Directory 496 * Server. 497 * @param saslProperties A set of additional properties that may be needed 498 * to process the SASL bind. 499 * @param requestControls The set of controls to include the request to the 500 * server. 501 * @param responseControls A list to hold the set of controls included in 502 * the response from the server. 503 * 504 * @return A message providing additional information about the bind if 505 * appropriate, or <CODE>null</CODE> if there is no special 506 * information available. 507 * 508 * @throws ClientException If a client-side problem prevents the bind 509 * attempt from succeeding. 510 * 511 * @throws LDAPException If the bind fails or some other server-side problem 512 * occurs during processing. 513 */ 514 private String doSASLCRAMMD5(ByteSequence bindDN, 515 ByteSequence bindPassword, 516 Map<String,List<String>> saslProperties, 517 List<Control> requestControls, 518 List<Control> responseControls) 519 throws ClientException, LDAPException 520 { 521 String authID = null; 522 523 524 // Evaluate the properties provided. The authID is required, no other 525 // properties are allowed. 526 if (saslProperties == null || saslProperties.isEmpty()) 527 { 528 LocalizableMessage message = 529 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_CRAM_MD5); 530 throw new ClientException( 531 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 532 } 533 534 for (Entry<String, List<String>> entry : saslProperties.entrySet()) 535 { 536 String name = entry.getKey(); 537 List<String> values = entry.getValue(); 538 String lowerName = toLowerCase(name); 539 540 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 541 { 542 authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED); 543 } 544 else 545 { 546 LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get( 547 name, SASL_MECHANISM_CRAM_MD5); 548 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 549 } 550 } 551 552 553 // Make sure that the authID was provided. 554 if (authID == null || authID.length() == 0) 555 { 556 LocalizableMessage message = 557 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_CRAM_MD5); 558 throw new ClientException( 559 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 560 } 561 562 563 // Set password to ByteString.empty if the password is null. 564 if (bindPassword == null) 565 { 566 bindPassword = ByteString.empty(); 567 } 568 569 sendInitialBindRequest(SASL_MECHANISM_CRAM_MD5, bindDN); 570 571 LDAPMessage responseMessage1 = 572 readBindResponse(ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE, SASL_MECHANISM_CRAM_MD5); 573 checkConnected(responseMessage1); 574 575 // Make sure that the bind response has the "SASL bind in progress" result code. 576 BindResponseProtocolOp bindResponse1 = 577 responseMessage1.getBindResponseProtocolOp(); 578 int resultCode1 = bindResponse1.getResultCode(); 579 if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get()) 580 { 581 LocalizableMessage errorMessage = bindResponse1.getErrorMessage(); 582 if (errorMessage == null) 583 { 584 errorMessage = LocalizableMessage.EMPTY; 585 } 586 587 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE. 588 get(SASL_MECHANISM_CRAM_MD5, resultCode1, 589 ReturnCode.get(resultCode1), errorMessage); 590 throw new LDAPException(resultCode1, errorMessage, message, 591 bindResponse1.getMatchedDN(), null); 592 } 593 594 595 // Make sure that the bind response contains SASL credentials with the 596 // challenge to use for the next stage of the bind. 597 ByteString serverChallenge = bindResponse1.getServerSASLCredentials(); 598 if (serverChallenge == null) 599 { 600 LocalizableMessage message = ERR_LDAPAUTH_NO_CRAMMD5_SERVER_CREDENTIALS.get(); 601 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 602 } 603 604 // Use the provided password and credentials to generate the CRAM-MD5 response. 605 String salsCredentials = authID + ' ' + generateCRAMMD5Digest(bindPassword, serverChallenge); 606 sendSecondBindRequest(SASL_MECHANISM_CRAM_MD5, bindDN, salsCredentials, requestControls); 607 608 LDAPMessage responseMessage2 = 609 readBindResponse(ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE, SASL_MECHANISM_CRAM_MD5); 610 responseControls.addAll(responseMessage2.getControls()); 611 checkConnected(responseMessage2); 612 checkSuccessfulBind(responseMessage2, SASL_MECHANISM_CRAM_MD5); 613 return null; 614 } 615 616 /** 617 * Construct the initial bind request to send to the server. We'll simply indicate the SASL 618 * mechanism we want to use so the server will send us the challenge. 619 */ 620 private void sendInitialBindRequest(String saslMechanism, ByteSequence bindDN) throws ClientException 621 { 622 // FIXME -- Should we include request controls in both stages or just the second stage? 623 BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, null); 624 LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest); 625 626 try 627 { 628 writer.writeMessage(requestMessage); 629 } 630 catch (IOException ioe) 631 { 632 LocalizableMessage message = 633 ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe)); 634 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 635 } 636 catch (Exception e) 637 { 638 LocalizableMessage message = 639 ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(saslMechanism, getExceptionMessage(e)); 640 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e); 641 } 642 } 643 644 private LDAPMessage readBindResponse(Arg2<Object, Object> errCannotReadBindResponse, String saslMechanism) 645 throws ClientException 646 { 647 try 648 { 649 LDAPMessage responseMessage = reader.readMessage(); 650 if (responseMessage != null) 651 { 652 return responseMessage; 653 } 654 LocalizableMessage message = ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 655 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message); 656 } 657 catch (DecodeException | LDAPException e) 658 { 659 LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(e)); 660 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 661 } 662 catch (IOException ioe) 663 { 664 LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(ioe)); 665 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 666 } 667 catch (Exception e) 668 { 669 LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(e)); 670 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 671 } 672 } 673 674 /** 675 * Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication 676 * with the given information. 677 * 678 * @param password The clear-text password to use when generating the 679 * digest. 680 * @param challenge The server-supplied challenge to use when generating the 681 * digest. 682 * 683 * @return The generated HMAC-MD5 digest for CRAM-MD5 authentication. 684 * 685 * @throws ClientException If a problem occurs while attempting to perform 686 * the necessary initialization. 687 */ 688 private String generateCRAMMD5Digest(ByteSequence password, 689 ByteSequence challenge) 690 throws ClientException 691 { 692 // Perform the necessary initialization if it hasn't been done yet. 693 if (md5Digest == null) 694 { 695 try 696 { 697 md5Digest = MessageDigest.getInstance("MD5"); 698 } 699 catch (Exception e) 700 { 701 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get( 702 getExceptionMessage(e)); 703 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 704 message, e); 705 } 706 } 707 708 if (iPad == null) 709 { 710 iPad = new byte[HMAC_MD5_BLOCK_LENGTH]; 711 oPad = new byte[HMAC_MD5_BLOCK_LENGTH]; 712 Arrays.fill(iPad, CRAMMD5_IPAD_BYTE); 713 Arrays.fill(oPad, CRAMMD5_OPAD_BYTE); 714 } 715 716 717 // Get the byte arrays backing the password and challenge. 718 byte[] p = password.toByteArray(); 719 byte[] c = challenge.toByteArray(); 720 721 722 // If the password is longer than the HMAC-MD5 block length, then use an 723 // MD5 digest of the password rather than the password itself. 724 if (password.length() > HMAC_MD5_BLOCK_LENGTH) 725 { 726 p = md5Digest.digest(p); 727 } 728 729 730 // Create byte arrays with data needed for the hash generation. 731 byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length]; 732 System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH); 733 System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length); 734 735 byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH]; 736 System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH); 737 738 739 // Iterate through the bytes in the key and XOR them with the iPad and 740 // oPad as appropriate. 741 for (int i=0; i < p.length; i++) 742 { 743 iPadAndData[i] ^= p[i]; 744 oPadAndHash[i] ^= p[i]; 745 } 746 747 748 // Copy an MD5 digest of the iPad-XORed key and the data into the array to 749 // be hashed. 750 System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash, 751 HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH); 752 753 754 // Calculate an MD5 digest of the resulting array and get the corresponding 755 // hex string representation. 756 byte[] digestBytes = md5Digest.digest(oPadAndHash); 757 758 StringBuilder hexDigest = new StringBuilder(2*digestBytes.length); 759 for (byte b : digestBytes) 760 { 761 hexDigest.append(byteToLowerHex(b)); 762 } 763 764 return hexDigest.toString(); 765 } 766 767 768 769 /** 770 * Retrieves the set of properties that a client may provide when performing a 771 * SASL CRAM-MD5 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 CRAM-MD5 bind, mapped from the property names to their 776 * corresponding descriptions. 777 */ 778 private static LinkedHashMap<String, LocalizableMessage> getSASLCRAMMD5Properties() 779 { 780 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1); 781 782 properties.put(SASL_PROPERTY_AUTHID, 783 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 784 785 return properties; 786 } 787 788 789 790 /** 791 * Processes a SASL DIGEST-MD5 bind with the provided information. 792 * 793 * @param bindDN The DN to use to bind to the Directory Server, or 794 * <CODE>null</CODE> if the authentication identity 795 * is to be set through some other means. 796 * @param bindPassword The password to use to bind to the Directory 797 * Server. 798 * @param saslProperties A set of additional properties that may be needed 799 * to process the SASL bind. 800 * @param requestControls The set of controls to include the request to the 801 * server. 802 * @param responseControls A list to hold the set of controls included in 803 * the response from the server. 804 * 805 * @return A message providing additional information about the bind if 806 * appropriate, or <CODE>null</CODE> if there is no special 807 * information available. 808 * 809 * @throws ClientException If a client-side problem prevents the bind 810 * attempt from succeeding. 811 * 812 * @throws LDAPException If the bind fails or some other server-side problem 813 * occurs during processing. 814 */ 815 private String doSASLDigestMD5(ByteSequence bindDN, 816 ByteSequence bindPassword, 817 Map<String,List<String>> saslProperties, 818 List<Control> requestControls, 819 List<Control> responseControls) 820 throws ClientException, LDAPException 821 { 822 String authID = null; 823 String realm = null; 824 String qop = "auth"; 825 String digestURI = "ldap/" + hostName; 826 String authzID = null; 827 boolean realmSetFromProperty = false; 828 829 830 // Evaluate the properties provided. The authID is required. The realm, 831 // QoP, digest URI, and authzID are optional. 832 if (saslProperties == null || saslProperties.isEmpty()) 833 { 834 LocalizableMessage message = 835 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_DIGEST_MD5); 836 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 837 } 838 839 for (Entry<String, List<String>> entry : saslProperties.entrySet()) 840 { 841 String name = entry.getKey(); 842 List<String> values = entry.getValue(); 843 String lowerName = toLowerCase(name); 844 845 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 846 { 847 authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED); 848 } 849 else if (lowerName.equals(SASL_PROPERTY_REALM)) 850 { 851 Iterator<String> iterator = values.iterator(); 852 if (iterator.hasNext()) 853 { 854 realm = iterator.next(); 855 realmSetFromProperty = true; 856 857 if (iterator.hasNext()) 858 { 859 LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get(); 860 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 861 message); 862 } 863 } 864 } 865 else if (lowerName.equals(SASL_PROPERTY_QOP)) 866 { 867 Iterator<String> iterator = values.iterator(); 868 if (iterator.hasNext()) 869 { 870 qop = toLowerCase(iterator.next()); 871 872 if (iterator.hasNext()) 873 { 874 LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get(); 875 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 876 message); 877 } 878 879 if (qop.equals("auth")) 880 { 881 // This is always fine. 882 } 883 else if (qop.equals("auth-int") || qop.equals("auth-conf")) 884 { 885 // FIXME -- Add support for integrity and confidentiality. 886 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(qop); 887 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 888 message); 889 } 890 else 891 { 892 // This is an illegal value. 893 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_QOP.get(qop); 894 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 895 message); 896 } 897 } 898 } 899 else if (lowerName.equals(SASL_PROPERTY_DIGEST_URI)) 900 { 901 digestURI = toLowerCase(getSingleValue(values, ERR_LDAPAUTH_DIGEST_URI_SINGLE_VALUED)); 902 } 903 else if (lowerName.equals(SASL_PROPERTY_AUTHZID)) 904 { 905 authzID = toLowerCase(getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED)); 906 } 907 else 908 { 909 LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get( 910 name, SASL_MECHANISM_DIGEST_MD5); 911 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 912 } 913 } 914 915 916 // Make sure that the authID was provided. 917 if (authID == null || authID.length() == 0) 918 { 919 LocalizableMessage message = 920 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_DIGEST_MD5); 921 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 922 message); 923 } 924 925 926 // Set password to ByteString.empty if the password is null. 927 if (bindPassword == null) 928 { 929 bindPassword = ByteString.empty(); 930 } 931 932 933 sendInitialBindRequest(SASL_MECHANISM_DIGEST_MD5, bindDN); 934 935 LDAPMessage responseMessage1 = 936 readBindResponse(ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE, SASL_MECHANISM_DIGEST_MD5); 937 checkConnected(responseMessage1); 938 939 // Make sure that the bind response has the "SASL bind in progress" result code. 940 BindResponseProtocolOp bindResponse1 = 941 responseMessage1.getBindResponseProtocolOp(); 942 int resultCode1 = bindResponse1.getResultCode(); 943 if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get()) 944 { 945 LocalizableMessage errorMessage = bindResponse1.getErrorMessage(); 946 if (errorMessage == null) 947 { 948 errorMessage = LocalizableMessage.EMPTY; 949 } 950 951 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE. 952 get(SASL_MECHANISM_DIGEST_MD5, resultCode1, 953 ReturnCode.get(resultCode1), errorMessage); 954 throw new LDAPException(resultCode1, errorMessage, message, 955 bindResponse1.getMatchedDN(), null); 956 } 957 958 959 // Make sure that the bind response contains SASL credentials with the 960 // information to use for the next stage of the bind. 961 ByteString serverCredentials = 962 bindResponse1.getServerSASLCredentials(); 963 if (serverCredentials == null) 964 { 965 LocalizableMessage message = ERR_LDAPAUTH_NO_DIGESTMD5_SERVER_CREDENTIALS.get(); 966 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 967 } 968 969 970 // Parse the server SASL credentials to get the necessary information. In 971 // particular, look at the realm, the nonce, the QoP modes, and the charset. 972 // We'll only care about the realm if none was provided in the SASL 973 // properties and only one was provided in the server SASL credentials. 974 String credString = serverCredentials.toString(); 975 String lowerCreds = toLowerCase(credString); 976 String nonce = null; 977 boolean useUTF8 = false; 978 int pos = 0; 979 int length = credString.length(); 980 while (pos < length) 981 { 982 int equalPos = credString.indexOf('=', pos+1); 983 if (equalPos < 0) 984 { 985 // This is bad because we're not at the end of the string but we don't 986 // have a name/value delimiter. 987 LocalizableMessage message = 988 ERR_LDAPAUTH_DIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get( 989 credString, pos); 990 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 991 } 992 993 994 String tokenName = lowerCreds.substring(pos, equalPos); 995 996 StringBuilder valueBuffer = new StringBuilder(); 997 pos = readToken(credString, equalPos+1, length, valueBuffer); 998 String tokenValue = valueBuffer.toString(); 999 1000 if (tokenName.equals("charset")) 1001 { 1002 // The value must be the string "utf-8". If not, that's an error. 1003 if (! tokenValue.equalsIgnoreCase("utf-8")) 1004 { 1005 LocalizableMessage message = 1006 ERR_LDAPAUTH_DIGESTMD5_INVALID_CHARSET.get(tokenValue); 1007 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1008 } 1009 1010 useUTF8 = true; 1011 } 1012 else if (tokenName.equals("realm")) 1013 { 1014 // This will only be of interest to us if there is only a single realm 1015 // in the server credentials and none was provided as a client-side 1016 // property. 1017 if (! realmSetFromProperty) 1018 { 1019 if (realm == null) 1020 { 1021 // No other realm was specified, so we'll use this one for now. 1022 realm = tokenValue; 1023 } 1024 else 1025 { 1026 // This must mean that there are multiple realms in the server 1027 // credentials. In that case, we'll not provide any realm at all. 1028 // To make sure that happens, pretend that the client specified the 1029 // realm. 1030 realm = null; 1031 realmSetFromProperty = true; 1032 } 1033 } 1034 } 1035 else if (tokenName.equals("nonce")) 1036 { 1037 nonce = tokenValue; 1038 } 1039 else if (tokenName.equals("qop")) 1040 { 1041 // The QoP modes provided by the server should be a comma-delimited 1042 // list. Decode that list and make sure the QoP we have chosen is in 1043 // that list. 1044 StringTokenizer tokenizer = new StringTokenizer(tokenValue, ","); 1045 LinkedList<String> qopModes = new LinkedList<>(); 1046 while (tokenizer.hasMoreTokens()) 1047 { 1048 qopModes.add(toLowerCase(tokenizer.nextToken().trim())); 1049 } 1050 1051 if (! qopModes.contains(qop)) 1052 { 1053 LocalizableMessage message = ERR_LDAPAUTH_REQUESTED_QOP_NOT_SUPPORTED_BY_SERVER. 1054 get(qop, tokenValue); 1055 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1056 message); 1057 } 1058 } 1059 else 1060 { 1061 // Other values may have been provided, but they aren't of interest to 1062 // us because they shouldn't change anything about the way we encode the 1063 // second part of the request. Rather than attempt to examine them, 1064 // we'll assume that the server sent a valid response. 1065 } 1066 } 1067 1068 1069 // Make sure that the nonce was included in the response from the server. 1070 if (nonce == null) 1071 { 1072 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_NONCE.get(); 1073 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1074 } 1075 1076 1077 // Generate the cnonce that we will use for this request. 1078 String cnonce = generateCNonce(); 1079 1080 1081 // Generate the response digest, and initialize the necessary remaining 1082 // variables to use in the generation of that digest. 1083 String nonceCount = "00000001"; 1084 String charset = useUTF8 ? "UTF-8" : "ISO-8859-1"; 1085 String responseDigest; 1086 try 1087 { 1088 responseDigest = generateDigestMD5Response(authID, authzID, 1089 bindPassword, realm, 1090 nonce, cnonce, nonceCount, 1091 digestURI, qop, charset); 1092 } 1093 catch (ClientException ce) 1094 { 1095 throw ce; 1096 } 1097 catch (Exception e) 1098 { 1099 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_CANNOT_CREATE_RESPONSE_DIGEST. 1100 get(getExceptionMessage(e)); 1101 throw new ClientException( 1102 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1103 } 1104 1105 1106 // Generate the SASL credentials for the second bind request. 1107 StringBuilder credBuffer = new StringBuilder(); 1108 credBuffer.append("username=\"").append(authID).append("\""); 1109 if (realm != null) 1110 { 1111 credBuffer.append(",realm=\"").append(realm).append("\""); 1112 } 1113 credBuffer.append(",nonce=\"").append(nonce); 1114 credBuffer.append("\",cnonce=\"").append(cnonce); 1115 credBuffer.append("\",nc=").append(nonceCount); 1116 credBuffer.append(",qop=").append(qop); 1117 credBuffer.append(",digest-uri=\"").append(digestURI); 1118 credBuffer.append("\",response=").append(responseDigest); 1119 if (useUTF8) 1120 { 1121 credBuffer.append(",charset=utf-8"); 1122 } 1123 if (authzID != null) 1124 { 1125 credBuffer.append(",authzid=\"").append(authzID).append("\""); 1126 } 1127 1128 sendSecondBindRequest(SASL_MECHANISM_DIGEST_MD5, bindDN, credBuffer.toString(), requestControls); 1129 1130 LDAPMessage responseMessage2 = 1131 readBindResponse(ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE, SASL_MECHANISM_DIGEST_MD5); 1132 responseControls.addAll(responseMessage2.getControls()); 1133 checkConnected(responseMessage2); 1134 BindResponseProtocolOp bindResponse2 = checkSuccessfulBind(responseMessage2, SASL_MECHANISM_DIGEST_MD5); 1135 1136 1137 // Make sure that the bind response included server SASL credentials with 1138 // the appropriate rspauth value. 1139 ByteString rspAuthCreds = bindResponse2.getServerSASLCredentials(); 1140 if (rspAuthCreds == null) 1141 { 1142 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get(); 1143 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1144 } 1145 1146 String credStr = toLowerCase(rspAuthCreds.toString()); 1147 if (! credStr.startsWith("rspauth=")) 1148 { 1149 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get(); 1150 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1151 } 1152 1153 1154 byte[] serverRspAuth; 1155 try 1156 { 1157 serverRspAuth = hexStringToByteArray(credStr.substring(8)); 1158 } 1159 catch (Exception e) 1160 { 1161 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_DECODE_RSPAUTH.get( 1162 getExceptionMessage(e)); 1163 throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message); 1164 } 1165 1166 byte[] clientRspAuth; 1167 try 1168 { 1169 clientRspAuth = 1170 generateDigestMD5RspAuth(authID, authzID, bindPassword, 1171 realm, nonce, cnonce, nonceCount, digestURI, 1172 qop, charset); 1173 } 1174 catch (Exception e) 1175 { 1176 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_CALCULATE_RSPAUTH.get( 1177 getExceptionMessage(e)); 1178 throw new ClientException( 1179 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1180 } 1181 1182 if (! Arrays.equals(serverRspAuth, clientRspAuth)) 1183 { 1184 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_RSPAUTH_MISMATCH.get(); 1185 throw new ClientException( 1186 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 1187 } 1188 1189 // FIXME -- Need to look for things like password expiration warning, reset notice, etc. 1190 return null; 1191 } 1192 1193 private void sendSecondBindRequest(String saslMechanism, ByteSequence bindDN, String saslCredentials, 1194 List<Control> requestControls) throws ClientException 1195 { 1196 // Generate and send the second bind request. 1197 BindRequestProtocolOp bindRequest2 = 1198 new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, ByteString.valueOfUtf8(saslCredentials)); 1199 LDAPMessage requestMessage2 = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2, requestControls); 1200 1201 try 1202 { 1203 writer.writeMessage(requestMessage2); 1204 } 1205 catch (IOException ioe) 1206 { 1207 LocalizableMessage message = 1208 ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe)); 1209 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 1210 } 1211 catch (Exception e) 1212 { 1213 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(saslMechanism, getExceptionMessage(e)); 1214 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e); 1215 } 1216 } 1217 1218 /** 1219 * Reads the next token from the provided credentials string using the 1220 * provided information. If the token is surrounded by quotation marks, then 1221 * the token returned will not include those quotation marks. 1222 * 1223 * @param credentials The credentials string from which to read the token. 1224 * @param startPos The position of the first character of the token to 1225 * read. 1226 * @param length The total number of characters in the credentials 1227 * string. 1228 * @param token The buffer into which the token is to be placed. 1229 * 1230 * @return The position at which the next token should start, or a value 1231 * greater than or equal to the length of the string if there are no 1232 * more tokens. 1233 * 1234 * @throws LDAPException If a problem occurs while attempting to read the 1235 * token. 1236 */ 1237 private int readToken(String credentials, int startPos, int length, 1238 StringBuilder token) 1239 throws LDAPException 1240 { 1241 // If the position is greater than or equal to the length, then we shouldn't 1242 // do anything. 1243 if (startPos >= length) 1244 { 1245 return startPos; 1246 } 1247 1248 1249 // Look at the first character to see if it's an empty string or the string 1250 // is quoted. 1251 boolean isEscaped = false; 1252 boolean isQuoted = false; 1253 int pos = startPos; 1254 char c = credentials.charAt(pos++); 1255 1256 if (c == ',') 1257 { 1258 // This must be a zero-length token, so we'll just return the next 1259 // position. 1260 return pos; 1261 } 1262 else if (c == '"') 1263 { 1264 // The string is quoted, so we'll ignore this character, and we'll keep 1265 // reading until we find the unescaped closing quote followed by a comma 1266 // or the end of the string. 1267 isQuoted = true; 1268 } 1269 else if (c == '\\') 1270 { 1271 // The next character is escaped, so we'll take it no matter what. 1272 isEscaped = true; 1273 } 1274 else 1275 { 1276 // The string is not quoted, and this is the first character. Store this 1277 // character and keep reading until we find a comma or the end of the 1278 // string. 1279 token.append(c); 1280 } 1281 1282 1283 // Enter a loop, reading until we find the appropriate criteria for the end 1284 // of the token. 1285 while (pos < length) 1286 { 1287 c = credentials.charAt(pos++); 1288 1289 if (isEscaped) 1290 { 1291 // The previous character was an escape, so we'll take this no matter 1292 // what. 1293 token.append(c); 1294 isEscaped = false; 1295 } 1296 else if (c == ',') 1297 { 1298 // If this is a quoted string, then this comma is part of the token. 1299 // Otherwise, it's the end of the token. 1300 if (!isQuoted) 1301 { 1302 break; 1303 } 1304 token.append(c); 1305 } 1306 else if (c == '"') 1307 { 1308 if (isQuoted) 1309 { 1310 // This should be the end of the token, but in order for it to be 1311 // valid it must be followed by a comma or the end of the string. 1312 if (pos >= length) 1313 { 1314 // We have hit the end of the string, so this is fine. 1315 break; 1316 } 1317 char c2 = credentials.charAt(pos++); 1318 if (c2 == ',') 1319 { 1320 // We have hit the end of the token, so this is fine. 1321 break; 1322 } 1323 else 1324 { 1325 // We found the closing quote before the end of the token. This is not fine. 1326 LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_CLOSING_QUOTE_POS.get(pos - 2); 1327 throw new LDAPException(ReturnCode.INVALID_CREDENTIALS.get(), message); 1328 } 1329 } 1330 else 1331 { 1332 // This must be part of the value, so we'll take it. 1333 token.append(c); 1334 } 1335 } 1336 else if (c == '\\') 1337 { 1338 // The next character is escaped. We'll set a flag so we know to 1339 // accept it, but will not include the backspace itself. 1340 isEscaped = true; 1341 } 1342 else 1343 { 1344 token.append(c); 1345 } 1346 } 1347 1348 1349 return pos; 1350 } 1351 1352 1353 1354 /** 1355 * Generates a cnonce value to use during the DIGEST-MD5 authentication 1356 * process. 1357 * 1358 * @return The cnonce that should be used for DIGEST-MD5 authentication. 1359 */ 1360 private String generateCNonce() 1361 { 1362 if (secureRandom == null) 1363 { 1364 secureRandom = new SecureRandom(); 1365 } 1366 1367 byte[] cnonceBytes = new byte[16]; 1368 secureRandom.nextBytes(cnonceBytes); 1369 1370 return Base64.encode(cnonceBytes); 1371 } 1372 1373 1374 1375 /** 1376 * Generates the appropriate DIGEST-MD5 response for the provided set of 1377 * information. 1378 * 1379 * @param authID The username from the authentication request. 1380 * @param authzID The authorization ID from the request, or 1381 * <CODE>null</CODE> if there is none. 1382 * @param password The clear-text password for the user. 1383 * @param realm The realm for which the authentication is to be 1384 * performed. 1385 * @param nonce The random data generated by the server for use in the 1386 * digest. 1387 * @param cnonce The random data generated by the client for use in the 1388 * digest. 1389 * @param nonceCount The 8-digit hex string indicating the number of times 1390 * the provided nonce has been used by the client. 1391 * @param digestURI The digest URI that specifies the service and host for 1392 * which the authentication is being performed. 1393 * @param qop The quality of protection string for the 1394 * authentication. 1395 * @param charset The character set used to encode the information. 1396 * 1397 * @return The DIGEST-MD5 response for the provided set of information. 1398 * 1399 * @throws ClientException If a problem occurs while attempting to 1400 * initialize the MD5 digest. 1401 * 1402 * @throws UnsupportedEncodingException If the specified character set is 1403 * invalid for some reason. 1404 */ 1405 private String generateDigestMD5Response(String authID, String authzID, 1406 ByteSequence password, String realm, 1407 String nonce, String cnonce, 1408 String nonceCount, String digestURI, 1409 String qop, String charset) 1410 throws ClientException, UnsupportedEncodingException 1411 { 1412 // Perform the necessary initialization if it hasn't been done yet. 1413 if (md5Digest == null) 1414 { 1415 try 1416 { 1417 md5Digest = MessageDigest.getInstance("MD5"); 1418 } 1419 catch (Exception e) 1420 { 1421 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get( 1422 getExceptionMessage(e)); 1423 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, 1424 message, e); 1425 } 1426 } 1427 1428 // Get a hash of "username:realm:password". 1429 String a1String1 = authID + ':' + ((realm == null) ? "" : realm) + ':'; 1430 byte[] a1Bytes1a = a1String1.getBytes(charset); 1431 byte[] a1Bytes1 = new byte[a1Bytes1a.length + password.length()]; 1432 System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length); 1433 password.copyTo(a1Bytes1, a1Bytes1a.length); 1434 byte[] urpHash = md5Digest.digest(a1Bytes1); 1435 1436 // Next, get a hash of "urpHash:nonce:cnonce[:authzid]". 1437 StringBuilder a1String2 = new StringBuilder(); 1438 a1String2.append(':'); 1439 a1String2.append(nonce); 1440 a1String2.append(':'); 1441 a1String2.append(cnonce); 1442 if (authzID != null) 1443 { 1444 a1String2.append(':'); 1445 a1String2.append(authzID); 1446 } 1447 byte[] a1Bytes2a = a1String2.toString().getBytes(charset); 1448 byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length]; 1449 System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length); 1450 System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length); 1451 byte[] a1Hash = md5Digest.digest(a1Bytes2); 1452 1453 // Next, get a hash of "AUTHENTICATE:digesturi". 1454 byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset); 1455 byte[] a2Hash = md5Digest.digest(a2Bytes); 1456 1457 // Get hex string representations of the last two hashes. 1458 String a1HashHex = getHexString(a1Hash); 1459 String a2HashHex = getHexString(a2Hash); 1460 1461 // Put together the final string to hash, consisting of 1462 // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest. 1463 String kdStr = a1HashHex + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + a2HashHex; 1464 return getHexString(md5Digest.digest(kdStr.getBytes(charset))); 1465 } 1466 1467 /** 1468 * Generates the appropriate DIGEST-MD5 rspauth digest using the provided 1469 * information. 1470 * 1471 * @param authID The username from the authentication request. 1472 * @param authzID The authorization ID from the request, or 1473 * <CODE>null</CODE> if there is none. 1474 * @param password The clear-text password for the user. 1475 * @param realm The realm for which the authentication is to be 1476 * performed. 1477 * @param nonce The random data generated by the server for use in the 1478 * digest. 1479 * @param cnonce The random data generated by the client for use in the 1480 * digest. 1481 * @param nonceCount The 8-digit hex string indicating the number of times 1482 * the provided nonce has been used by the client. 1483 * @param digestURI The digest URI that specifies the service and host for 1484 * which the authentication is being performed. 1485 * @param qop The quality of protection string for the 1486 * authentication. 1487 * @param charset The character set used to encode the information. 1488 * 1489 * @return The DIGEST-MD5 response for the provided set of information. 1490 * 1491 * @throws UnsupportedEncodingException If the specified character set is 1492 * invalid for some reason. 1493 */ 1494 private byte[] generateDigestMD5RspAuth(String authID, String authzID, 1495 ByteSequence password, String realm, 1496 String nonce, String cnonce, 1497 String nonceCount, String digestURI, 1498 String qop, String charset) 1499 throws UnsupportedEncodingException 1500 { 1501 // First, get a hash of "username:realm:password". 1502 String a1String1 = authID + ':' + realm + ':'; 1503 1504 byte[] a1Bytes1a = a1String1.getBytes(charset); 1505 byte[] a1Bytes1 = new byte[a1Bytes1a.length + password.length()]; 1506 System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length); 1507 password.copyTo(a1Bytes1, a1Bytes1a.length); 1508 byte[] urpHash = md5Digest.digest(a1Bytes1); 1509 1510 1511 // Next, get a hash of "urpHash:nonce:cnonce[:authzid]". 1512 StringBuilder a1String2 = new StringBuilder(); 1513 a1String2.append(':'); 1514 a1String2.append(nonce); 1515 a1String2.append(':'); 1516 a1String2.append(cnonce); 1517 if (authzID != null) 1518 { 1519 a1String2.append(':'); 1520 a1String2.append(authzID); 1521 } 1522 byte[] a1Bytes2a = a1String2.toString().getBytes(charset); 1523 byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length]; 1524 System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length); 1525 System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, 1526 a1Bytes2a.length); 1527 byte[] a1Hash = md5Digest.digest(a1Bytes2); 1528 1529 1530 // Next, get a hash of "AUTHENTICATE:digesturi". 1531 String a2String = ":" + digestURI; 1532 if (qop.equals("auth-int") || qop.equals("auth-conf")) 1533 { 1534 a2String += ":00000000000000000000000000000000"; 1535 } 1536 byte[] a2Bytes = a2String.getBytes(charset); 1537 byte[] a2Hash = md5Digest.digest(a2Bytes); 1538 1539 1540 // Get hex string representations of the last two hashes. 1541 String a1HashHex = getHexString(a1Hash); 1542 String a2HashHex = getHexString(a2Hash); 1543 1544 // Put together the final string to hash, consisting of 1545 // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest. 1546 String kdStr = a1HashHex + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + a2HashHex; 1547 return md5Digest.digest(kdStr.getBytes(charset)); 1548 } 1549 1550 /** 1551 * Retrieves a hexadecimal string representation of the contents of the 1552 * provided byte array. 1553 * 1554 * @param byteArray The byte array for which to obtain the hexadecimal 1555 * string representation. 1556 * 1557 * @return The hexadecimal string representation of the contents of the 1558 * provided byte array. 1559 */ 1560 private String getHexString(byte[] byteArray) 1561 { 1562 StringBuilder buffer = new StringBuilder(2*byteArray.length); 1563 for (byte b : byteArray) 1564 { 1565 buffer.append(byteToLowerHex(b)); 1566 } 1567 1568 return buffer.toString(); 1569 } 1570 1571 1572 1573 /** 1574 * Retrieves the set of properties that a client may provide when performing a 1575 * SASL DIGEST-MD5 bind, mapped from the property names to their corresponding 1576 * descriptions. 1577 * 1578 * @return The set of properties that a client may provide when performing a 1579 * SASL DIGEST-MD5 bind, mapped from the property names to their 1580 * corresponding descriptions. 1581 */ 1582 private static LinkedHashMap<String, LocalizableMessage> getSASLDigestMD5Properties() 1583 { 1584 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(5); 1585 1586 properties.put(SASL_PROPERTY_AUTHID, 1587 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 1588 properties.put(SASL_PROPERTY_REALM, 1589 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get()); 1590 properties.put(SASL_PROPERTY_QOP, 1591 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_QOP.get()); 1592 properties.put(SASL_PROPERTY_DIGEST_URI, 1593 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_DIGEST_URI.get()); 1594 properties.put(SASL_PROPERTY_AUTHZID, 1595 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get()); 1596 1597 return properties; 1598 } 1599 1600 1601 1602 /** 1603 * Processes a SASL EXTERNAL bind with the provided information. 1604 * 1605 * @param bindDN The DN to use to bind to the Directory Server, or 1606 * <CODE>null</CODE> if the authentication identity 1607 * is to be set through some other means. 1608 * @param saslProperties A set of additional properties that may be needed 1609 * to process the SASL bind. SASL EXTERNAL does not 1610 * take any properties, so this should be empty or 1611 * <CODE>null</CODE>. 1612 * @param requestControls The set of controls to include the request to the 1613 * server. 1614 * @param responseControls A list to hold the set of controls included in 1615 * the response from the server. 1616 * 1617 * @return A message providing additional information about the bind if 1618 * appropriate, or <CODE>null</CODE> if there is no special 1619 * information available. 1620 * 1621 * @throws ClientException If a client-side problem prevents the bind 1622 * attempt from succeeding. 1623 * 1624 * @throws LDAPException If the bind fails or some other server-side problem 1625 * occurs during processing. 1626 */ 1627 public String doSASLExternal(ByteSequence bindDN, 1628 Map<String,List<String>> saslProperties, 1629 List<Control> requestControls, 1630 List<Control> responseControls) 1631 throws ClientException, LDAPException 1632 { 1633 // Make sure that no SASL properties were provided. 1634 if (saslProperties != null && ! saslProperties.isEmpty()) 1635 { 1636 LocalizableMessage message = 1637 ERR_LDAPAUTH_NO_ALLOWED_SASL_PROPERTIES.get(SASL_MECHANISM_EXTERNAL); 1638 throw new ClientException( 1639 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 1640 } 1641 1642 1643 sendBindRequest(SASL_MECHANISM_EXTERNAL, bindDN, null, requestControls); 1644 1645 LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE); 1646 responseControls.addAll(responseMessage.getControls()); 1647 checkConnected(responseMessage); 1648 1649 BindResponseProtocolOp bindResponse = 1650 responseMessage.getBindResponseProtocolOp(); 1651 int resultCode = bindResponse.getResultCode(); 1652 if (resultCode == ReturnCode.SUCCESS.get()) 1653 { 1654 // FIXME -- Need to look for things like password expiration warning, 1655 // reset notice, etc. 1656 return null; 1657 } 1658 1659 // FIXME -- Add support for referrals. 1660 1661 LocalizableMessage message = 1662 ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_EXTERNAL); 1663 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), 1664 message, bindResponse.getMatchedDN(), null); 1665 } 1666 1667 private void sendBindRequest(String saslMechanism, ByteSequence bindDN, ByteString saslCredentials, 1668 List<Control> requestControls) throws ClientException 1669 { 1670 BindRequestProtocolOp bindRequest = 1671 new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, saslCredentials); 1672 LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, requestControls); 1673 1674 try 1675 { 1676 writer.writeMessage(requestMessage); 1677 } 1678 catch (IOException ioe) 1679 { 1680 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe)); 1681 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 1682 } 1683 catch (Exception e) 1684 { 1685 LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(saslMechanism, getExceptionMessage(e)); 1686 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e); 1687 } 1688 } 1689 1690 private LDAPMessage readBindResponse(Arg1<Object> errCannotReadBindResponse) throws ClientException 1691 { 1692 try 1693 { 1694 LDAPMessage responseMessage = reader.readMessage(); 1695 if (responseMessage != null) 1696 { 1697 return responseMessage; 1698 } 1699 LocalizableMessage message = ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get(); 1700 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message); 1701 } 1702 catch (DecodeException | LDAPException e) 1703 { 1704 LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(e)); 1705 throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e); 1706 } 1707 catch (IOException ioe) 1708 { 1709 LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(ioe)); 1710 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe); 1711 } 1712 catch (Exception e) 1713 { 1714 LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(e)); 1715 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1716 } 1717 } 1718 1719 /** 1720 * Retrieves the set of properties that a client may provide when performing a 1721 * SASL EXTERNAL bind, mapped from the property names to their corresponding 1722 * descriptions. 1723 * 1724 * @return The set of properties that a client may provide when performing a 1725 * SASL EXTERNAL bind, mapped from the property names to their 1726 * corresponding descriptions. 1727 */ 1728 private static LinkedHashMap<String, LocalizableMessage> getSASLExternalProperties() 1729 { 1730 // There are no properties for the SASL EXTERNAL mechanism. 1731 return new LinkedHashMap<>(0); 1732 } 1733 1734 1735 1736 /** 1737 * Processes a SASL GSSAPI bind with the provided information. 1738 * 1739 * @param bindDN The DN to use to bind to the Directory Server, or 1740 * <CODE>null</CODE> if the authentication identity 1741 * is to be set through some other means. 1742 * @param bindPassword The password to use to bind to the Directory 1743 * Server. 1744 * @param saslProperties A set of additional properties that may be needed 1745 * to process the SASL bind. SASL EXTERNAL does not 1746 * take any properties, so this should be empty or 1747 * <CODE>null</CODE>. 1748 * @param requestControls The set of controls to include the request to the 1749 * server. 1750 * @param responseControls A list to hold the set of controls included in 1751 * the response from the server. 1752 * 1753 * @return A message providing additional information about the bind if 1754 * appropriate, or <CODE>null</CODE> if there is no special 1755 * information available. 1756 * 1757 * @throws ClientException If a client-side problem prevents the bind 1758 * attempt from succeeding. 1759 * 1760 * @throws LDAPException If the bind fails or some other server-side problem 1761 * occurs during processing. 1762 */ 1763 private String doSASLGSSAPI(ByteSequence bindDN, 1764 ByteSequence bindPassword, 1765 Map<String,List<String>> saslProperties, 1766 List<Control> requestControls, 1767 List<Control> responseControls) 1768 throws ClientException, LDAPException 1769 { 1770 String kdc = null; 1771 String realm = null; 1772 1773 gssapiBindDN = bindDN; 1774 gssapiAuthID = null; 1775 gssapiAuthzID = null; 1776 gssapiQoP = "auth"; 1777 gssapiAuthPW = bindPassword != null ? bindPassword.toString().toCharArray() : null; 1778 1779 // Evaluate the properties provided. The authID is required. The authzID, 1780 // KDC, QoP, and realm are optional. 1781 if (saslProperties == null || saslProperties.isEmpty()) 1782 { 1783 LocalizableMessage message = 1784 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_GSSAPI); 1785 throw new ClientException( 1786 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 1787 } 1788 1789 for (Entry<String, List<String>> entry : saslProperties.entrySet()) 1790 { 1791 String name = entry.getKey(); 1792 String lowerName = toLowerCase(name); 1793 List<String> values = entry.getValue(); 1794 1795 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 1796 { 1797 gssapiAuthID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED); 1798 } 1799 else if (lowerName.equals(SASL_PROPERTY_AUTHZID)) 1800 { 1801 gssapiAuthzID = getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED); 1802 } 1803 else if (lowerName.equals(SASL_PROPERTY_KDC)) 1804 { 1805 kdc = getSingleValue(values, ERR_LDAPAUTH_KDC_SINGLE_VALUED); 1806 } 1807 else if (lowerName.equals(SASL_PROPERTY_QOP)) 1808 { 1809 Iterator<String> iterator = values.iterator(); 1810 if (iterator.hasNext()) 1811 { 1812 gssapiQoP = toLowerCase(iterator.next()); 1813 1814 if (iterator.hasNext()) 1815 { 1816 LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get(); 1817 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1818 message); 1819 } 1820 1821 if (gssapiQoP.equals("auth")) 1822 { 1823 // This is always fine. 1824 } 1825 else if (gssapiQoP.equals("auth-int") || 1826 gssapiQoP.equals("auth-conf")) 1827 { 1828 // FIXME -- Add support for integrity and confidentiality. 1829 LocalizableMessage message = 1830 ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(gssapiQoP); 1831 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1832 message); 1833 } 1834 else 1835 { 1836 // This is an illegal value. 1837 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_INVALID_QOP.get(gssapiQoP); 1838 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, 1839 message); 1840 } 1841 } 1842 } 1843 else if (lowerName.equals(SASL_PROPERTY_REALM)) 1844 { 1845 realm = getSingleValue(values, ERR_LDAPAUTH_REALM_SINGLE_VALUED); 1846 } 1847 else 1848 { 1849 LocalizableMessage message = 1850 ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_GSSAPI); 1851 throw new ClientException( 1852 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 1853 } 1854 } 1855 1856 1857 // Make sure that the authID was provided. 1858 if (gssapiAuthID == null || gssapiAuthID.length() == 0) 1859 { 1860 LocalizableMessage message = 1861 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_GSSAPI); 1862 throw new ClientException( 1863 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 1864 } 1865 1866 1867 // See if an authzID was provided. If not, then use the authID. 1868 if (gssapiAuthzID == null) 1869 { 1870 gssapiAuthzID = gssapiAuthID; 1871 } 1872 1873 1874 // See if the realm and/or KDC were specified. If so, then set properties 1875 // that will allow them to be used. Otherwise, we'll hope that the 1876 // underlying system has a valid Kerberos client configuration. 1877 if (realm != null) 1878 { 1879 System.setProperty(KRBV_PROPERTY_REALM, realm); 1880 } 1881 1882 if (kdc != null) 1883 { 1884 System.setProperty(KRBV_PROPERTY_KDC, kdc); 1885 } 1886 1887 1888 // Since we're going to be using JAAS behind the scenes, we need to have a 1889 // JAAS configuration. Rather than always requiring the user to provide it, 1890 // we'll write one to a temporary file that will be deleted when the JVM 1891 // exits. 1892 String configFileName; 1893 try 1894 { 1895 File tempFile = File.createTempFile("login", "conf"); 1896 configFileName = tempFile.getAbsolutePath(); 1897 tempFile.deleteOnExit(); 1898 try (BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false))) { 1899 w.write(getClass().getName() + " {"); 1900 w.newLine(); 1901 1902 w.write(" com.sun.security.auth.module.Krb5LoginModule required " + 1903 "client=TRUE useTicketCache=TRUE;"); 1904 w.newLine(); 1905 1906 w.write("};"); 1907 w.newLine(); 1908 } 1909 } 1910 catch (Exception e) 1911 { 1912 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get( 1913 getExceptionMessage(e)); 1914 throw new ClientException( 1915 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1916 } 1917 1918 System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName); 1919 System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "true"); 1920 1921 1922 // The rest of this code must be executed via JAAS, so it will have to go 1923 // in the "run" method. 1924 LoginContext loginContext; 1925 try 1926 { 1927 loginContext = new LoginContext(getClass().getName(), this); 1928 loginContext.login(); 1929 } 1930 catch (Exception e) 1931 { 1932 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED.get( 1933 getExceptionMessage(e)); 1934 throw new ClientException( 1935 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1936 } 1937 1938 try 1939 { 1940 Subject.doAs(loginContext.getSubject(), this); 1941 } 1942 catch (Exception e) 1943 { 1944 if (e instanceof ClientException) 1945 { 1946 throw (ClientException) e; 1947 } 1948 else if (e instanceof LDAPException) 1949 { 1950 throw (LDAPException) e; 1951 } 1952 1953 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_REMOTE_AUTHENTICATION_FAILED.get( 1954 getExceptionMessage(e)); 1955 throw new ClientException( 1956 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 1957 } 1958 1959 1960 // FIXME -- Need to make sure we handle request and response controls properly, 1961 // and also check for any possible message to send back to the client. 1962 return null; 1963 } 1964 1965 private String getSingleValue(List<String> values, Arg0 singleValuedErrMsg) throws ClientException 1966 { 1967 Iterator<String> it = values.iterator(); 1968 if (it.hasNext()) 1969 { 1970 String result = it.next(); 1971 if (it.hasNext()) 1972 { 1973 throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, singleValuedErrMsg.get()); 1974 } 1975 return result; 1976 } 1977 return null; 1978 } 1979 1980 /** 1981 * Retrieves the set of properties that a client may provide when performing a 1982 * SASL EXTERNAL bind, mapped from the property names to their corresponding 1983 * descriptions. 1984 * 1985 * @return The set of properties that a client may provide when performing a 1986 * SASL EXTERNAL bind, mapped from the property names to their 1987 * corresponding descriptions. 1988 */ 1989 private static LinkedHashMap<String, LocalizableMessage> getSASLGSSAPIProperties() 1990 { 1991 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(4); 1992 1993 properties.put(SASL_PROPERTY_AUTHID, 1994 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 1995 properties.put(SASL_PROPERTY_AUTHZID, 1996 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get()); 1997 properties.put(SASL_PROPERTY_KDC, 1998 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_KDC.get()); 1999 properties.put(SASL_PROPERTY_REALM, 2000 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get()); 2001 2002 return properties; 2003 } 2004 2005 2006 2007 /** 2008 * Processes a SASL PLAIN bind with the provided information. 2009 * 2010 * @param bindDN The DN to use to bind to the Directory Server, or 2011 * <CODE>null</CODE> if the authentication identity 2012 * is to be set through some other means. 2013 * @param bindPassword The password to use to bind to the Directory 2014 * Server. 2015 * @param saslProperties A set of additional properties that may be needed 2016 * to process the SASL bind. 2017 * @param requestControls The set of controls to include the request to the 2018 * server. 2019 * @param responseControls A list to hold the set of controls included in 2020 * the response from the server. 2021 * 2022 * @return A message providing additional information about the bind if 2023 * appropriate, or <CODE>null</CODE> if there is no special 2024 * information available. 2025 * 2026 * @throws ClientException If a client-side problem prevents the bind 2027 * attempt from succeeding. 2028 * 2029 * @throws LDAPException If the bind fails or some other server-side problem 2030 * occurs during processing. 2031 */ 2032 public String doSASLPlain(ByteSequence bindDN, 2033 ByteSequence bindPassword, 2034 Map<String,List<String>> saslProperties, 2035 List<Control> requestControls, 2036 List<Control> responseControls) 2037 throws ClientException, LDAPException 2038 { 2039 String authID = null; 2040 String authzID = null; 2041 2042 2043 // Evaluate the properties provided. The authID is required, and authzID is 2044 // optional. 2045 if (saslProperties == null || saslProperties.isEmpty()) 2046 { 2047 LocalizableMessage message = 2048 ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_PLAIN); 2049 throw new ClientException( 2050 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2051 } 2052 2053 for (Entry<String, List<String>> entry : saslProperties.entrySet()) 2054 { 2055 String name = entry.getKey(); 2056 List<String> values = entry.getValue(); 2057 String lowerName = toLowerCase(name); 2058 2059 if (lowerName.equals(SASL_PROPERTY_AUTHID)) 2060 { 2061 authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED); 2062 } 2063 else if (lowerName.equals(SASL_PROPERTY_AUTHZID)) 2064 { 2065 authzID = getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED); 2066 } 2067 else 2068 { 2069 LocalizableMessage message = 2070 ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_PLAIN); 2071 throw new ClientException( 2072 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2073 } 2074 } 2075 2076 2077 // Make sure that at least the authID was provided. 2078 if (authID == null || authID.length() == 0) 2079 { 2080 LocalizableMessage message = 2081 ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_PLAIN); 2082 throw new ClientException( 2083 ReturnCode.CLIENT_SIDE_PARAM_ERROR, message); 2084 } 2085 2086 2087 // Set password to ByteString.empty if the password is null. 2088 if (bindPassword == null) 2089 { 2090 bindPassword = ByteString.empty(); 2091 } 2092 2093 // Construct the bind request and send it to the server. 2094 String saslCredentials = (authzID != null ? authzID : "") + '\u0000' + authID + '\u0000' + bindPassword; 2095 sendBindRequest(SASL_MECHANISM_PLAIN, bindDN, ByteString.valueOfUtf8(saslCredentials), requestControls); 2096 2097 LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE); 2098 responseControls.addAll(responseMessage.getControls()); 2099 checkConnected(responseMessage); 2100 checkSuccessfulBind(responseMessage, SASL_MECHANISM_PLAIN); 2101 return null; 2102 } 2103 2104 /** 2105 * Retrieves the set of properties that a client may provide when performing a 2106 * SASL PLAIN bind, mapped from the property names to their corresponding 2107 * descriptions. 2108 * 2109 * @return The set of properties that a client may provide when performing a 2110 * SASL PLAIN bind, mapped from the property names to their 2111 * corresponding descriptions. 2112 */ 2113 private static LinkedHashMap<String, LocalizableMessage> getSASLPlainProperties() 2114 { 2115 LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(2); 2116 2117 properties.put(SASL_PROPERTY_AUTHID, 2118 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get()); 2119 properties.put(SASL_PROPERTY_AUTHZID, 2120 INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get()); 2121 2122 return properties; 2123 } 2124 2125 2126 2127 /** 2128 * Performs a privileged operation under JAAS so that the local authentication 2129 * information can be available for the SASL bind to the Directory Server. 2130 * 2131 * @return A placeholder object in order to comply with the 2132 * <CODE>PrivilegedExceptionAction</CODE> interface. 2133 * 2134 * @throws ClientException If a client-side problem occurs during the bind 2135 * processing. 2136 * 2137 * @throws LDAPException If a server-side problem occurs during the bind 2138 * processing. 2139 */ 2140 @Override 2141 public Object run() throws ClientException, LDAPException 2142 { 2143 if (saslMechanism == null) 2144 { 2145 LocalizableMessage message = ERR_LDAPAUTH_NONSASL_RUN_INVOCATION.get(getBacktrace()); 2146 throw new ClientException( 2147 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2148 } 2149 else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI)) 2150 { 2151 doSASLGSSAPI2(); 2152 return null; 2153 } 2154 else 2155 { 2156 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RUN_INVOCATION.get( 2157 saslMechanism, getBacktrace()); 2158 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2159 } 2160 } 2161 2162 private void doSASLGSSAPI2() throws ClientException, LDAPException 2163 { 2164 // Create the property map that will be used by the internal SASL handler. 2165 Map<String, String> saslProperties = new HashMap<>(); 2166 saslProperties.put(Sasl.QOP, gssapiQoP); 2167 saslProperties.put(Sasl.SERVER_AUTH, "true"); 2168 2169 2170 // Create the SASL client that we will use to actually perform the 2171 // authentication. 2172 SaslClient saslClient; 2173 try 2174 { 2175 saslClient = 2176 Sasl.createSaslClient(new String[] { SASL_MECHANISM_GSSAPI }, 2177 gssapiAuthzID, "ldap", hostName, 2178 saslProperties, this); 2179 } 2180 catch (Exception e) 2181 { 2182 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get( 2183 getExceptionMessage(e)); 2184 throw new ClientException( 2185 ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2186 } 2187 2188 // FIXME -- Add controls here? 2189 ByteString saslCredentials = getSaslCredentialsForInitialBind(saslClient); 2190 sendBindRequest(SASL_MECHANISM_GSSAPI, gssapiBindDN, saslCredentials, null); 2191 2192 LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE); 2193 // FIXME -- Handle response controls. 2194 checkConnected(responseMessage); 2195 2196 while (true) 2197 { 2198 BindResponseProtocolOp bindResponse = 2199 responseMessage.getBindResponseProtocolOp(); 2200 int resultCode = bindResponse.getResultCode(); 2201 if (resultCode == ReturnCode.SUCCESS.get()) 2202 { 2203 evaluateGSSAPIChallenge(saslClient, bindResponse); 2204 break; 2205 } 2206 else if (resultCode == ReturnCode.SASL_BIND_IN_PROGRESS.get()) 2207 { 2208 // FIXME -- Add controls here? 2209 ByteString credBytes = evaluateSaslChallenge(saslClient, bindResponse); 2210 sendBindRequest(SASL_MECHANISM_GSSAPI, gssapiBindDN, credBytes, null); 2211 2212 responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE); 2213 // FIXME -- Handle response controls. 2214 checkConnected(responseMessage); 2215 } 2216 else 2217 { 2218 // This is an error. 2219 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get(); 2220 throw new LDAPException(resultCode, bindResponse.getErrorMessage(), 2221 message, bindResponse.getMatchedDN(), 2222 null); 2223 } 2224 } 2225 // FIXME -- Need to look for things like password expiration warning, reset notice, etc. 2226 } 2227 2228 2229 private void evaluateGSSAPIChallenge(SaslClient saslClient, BindResponseProtocolOp bindResponse) 2230 throws ClientException 2231 { 2232 // We should be done after this, but we still need to look for and 2233 // handle the server SASL credentials. 2234 ByteString serverSASLCredentials = bindResponse.getServerSASLCredentials(); 2235 if (serverSASLCredentials != null) 2236 { 2237 try 2238 { 2239 saslClient.evaluateChallenge(serverSASLCredentials.toByteArray()); 2240 } 2241 catch (Exception e) 2242 { 2243 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.get(getExceptionMessage(e)); 2244 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2245 } 2246 } 2247 2248 // Just to be sure, check that the login really is complete. 2249 if (!saslClient.isComplete()) 2250 { 2251 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_UNEXPECTED_SUCCESS_RESPONSE.get(); 2252 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2253 } 2254 } 2255 2256 private ByteString evaluateSaslChallenge(SaslClient saslClient, BindResponseProtocolOp bindResponse) 2257 throws ClientException 2258 { 2259 try 2260 { 2261 ByteString saslCredentials = bindResponse.getServerSASLCredentials(); 2262 byte[] bs = saslCredentials != null ? saslCredentials.toByteArray() : new byte[0]; 2263 return ByteString.wrap(saslClient.evaluateChallenge(bs)); 2264 } 2265 catch (Exception e) 2266 { 2267 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.get(getExceptionMessage(e)); 2268 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2269 } 2270 } 2271 2272 private ByteString getSaslCredentialsForInitialBind(SaslClient saslClient) throws ClientException 2273 { 2274 if (saslClient.hasInitialResponse()) 2275 { 2276 try 2277 { 2278 return ByteString.wrap(saslClient.evaluateChallenge(new byte[0])); 2279 } 2280 catch (Exception e) 2281 { 2282 LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_INITIAL_CHALLENGE.get(getExceptionMessage(e)); 2283 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e); 2284 } 2285 } 2286 return null; 2287 } 2288 2289 /** 2290 * Look at the protocol op from the response. 2291 * If it's a bind response, then continue. 2292 * If it's an extended response, then check it is not a notice of disconnection. 2293 * Otherwise, generate an error. 2294 */ 2295 private void checkConnected(LDAPMessage responseMessage) throws LDAPException, ClientException 2296 { 2297 switch (responseMessage.getProtocolOpType()) 2298 { 2299 case OP_TYPE_BIND_RESPONSE: 2300 // We'll deal with this later. 2301 break; 2302 2303 case OP_TYPE_EXTENDED_RESPONSE: 2304 ExtendedResponseProtocolOp extendedResponse = 2305 responseMessage.getExtendedResponseProtocolOp(); 2306 String responseOID = extendedResponse.getOID(); 2307 if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID)) 2308 { 2309 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT. 2310 get(extendedResponse.getResultCode(), extendedResponse.getErrorMessage()); 2311 throw new LDAPException(extendedResponse.getResultCode(), message); 2312 } 2313 else 2314 { 2315 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse); 2316 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2317 } 2318 2319 default: 2320 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp()); 2321 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2322 } 2323 } 2324 2325 /** 2326 * Handles the authentication callbacks to provide information needed by the 2327 * JAAS login process. 2328 * 2329 * @param callbacks The callbacks needed to provide information for the JAAS 2330 * login process. 2331 * 2332 * @throws UnsupportedCallbackException If an unexpected callback is 2333 * included in the provided set. 2334 */ 2335 @Override 2336 public void handle(Callback[] callbacks) 2337 throws UnsupportedCallbackException 2338 { 2339 if (saslMechanism == null) 2340 { 2341 LocalizableMessage message = 2342 ERR_LDAPAUTH_NONSASL_CALLBACK_INVOCATION.get(getBacktrace()); 2343 throw new UnsupportedCallbackException(callbacks[0], message.toString()); 2344 } 2345 else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI)) 2346 { 2347 for (Callback cb : callbacks) 2348 { 2349 if (cb instanceof NameCallback) 2350 { 2351 ((NameCallback) cb).setName(gssapiAuthID); 2352 } 2353 else if (cb instanceof PasswordCallback) 2354 { 2355 if (gssapiAuthPW == null) 2356 { 2357 System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(gssapiAuthID)); 2358 try 2359 { 2360 gssapiAuthPW = ConsoleApplication.readPassword(); 2361 } 2362 catch (ClientException e) 2363 { 2364 throw new UnsupportedCallbackException(cb, e.getLocalizedMessage()); 2365 } 2366 } 2367 2368 ((PasswordCallback) cb).setPassword(gssapiAuthPW); 2369 } 2370 else 2371 { 2372 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_GSSAPI_CALLBACK.get(cb); 2373 throw new UnsupportedCallbackException(cb, message.toString()); 2374 } 2375 } 2376 } 2377 else 2378 { 2379 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_CALLBACK_INVOCATION.get( 2380 saslMechanism, getBacktrace()); 2381 throw new UnsupportedCallbackException(callbacks[0], message.toString()); 2382 } 2383 } 2384 2385 2386 2387 /** 2388 * Uses the "Who Am I?" extended operation to request that the server provide 2389 * the client with the authorization identity for this connection. 2390 * 2391 * @return An ASN.1 octet string containing the authorization identity, or 2392 * <CODE>null</CODE> if the client is not authenticated or is 2393 * authenticated anonymously. 2394 * 2395 * @throws ClientException If a client-side problem occurs during the 2396 * request processing. 2397 * 2398 * @throws LDAPException If a server-side problem occurs during the request 2399 * processing. 2400 */ 2401 public ByteString requestAuthorizationIdentity() 2402 throws ClientException, LDAPException 2403 { 2404 // Construct the extended request and send it to the server. 2405 ExtendedRequestProtocolOp extendedRequest = 2406 new ExtendedRequestProtocolOp(OID_WHO_AM_I_REQUEST); 2407 LDAPMessage requestMessage = 2408 new LDAPMessage(nextMessageID.getAndIncrement(), extendedRequest); 2409 2410 try 2411 { 2412 writer.writeMessage(requestMessage); 2413 } 2414 catch (IOException ioe) 2415 { 2416 LocalizableMessage message = 2417 ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(ioe)); 2418 throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, 2419 message, ioe); 2420 } 2421 catch (Exception e) 2422 { 2423 LocalizableMessage message = 2424 ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(e)); 2425 throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, 2426 message, e); 2427 } 2428 2429 2430 LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE); 2431 2432 // If the protocol op isn't an extended response, then that's a problem. 2433 if (responseMessage.getProtocolOpType() != OP_TYPE_EXTENDED_RESPONSE) 2434 { 2435 LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp()); 2436 throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message); 2437 } 2438 2439 2440 // Get the extended response and see if it has the "notice of disconnection" 2441 // OID. If so, then the server is closing the connection. 2442 ExtendedResponseProtocolOp extendedResponse = 2443 responseMessage.getExtendedResponseProtocolOp(); 2444 String responseOID = extendedResponse.getOID(); 2445 if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID)) 2446 { 2447 LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.get( 2448 extendedResponse.getResultCode(), extendedResponse.getErrorMessage()); 2449 throw new LDAPException(extendedResponse.getResultCode(), message); 2450 } 2451 2452 2453 // It isn't a notice of disconnection so it must be the "Who Am I?" 2454 // response and the value would be the authorization ID. However, first 2455 // check that it was successful. If it was not, then fail. 2456 int resultCode = extendedResponse.getResultCode(); 2457 if (resultCode != ReturnCode.SUCCESS.get()) 2458 { 2459 LocalizableMessage message = ERR_LDAPAUTH_WHOAMI_FAILED.get(); 2460 throw new LDAPException(resultCode, extendedResponse.getErrorMessage(), 2461 message, extendedResponse.getMatchedDN(), 2462 null); 2463 } 2464 2465 2466 // Get the authorization ID (if there is one) and return it to the caller. 2467 ByteString authzID = extendedResponse.getValue(); 2468 if (authzID == null || authzID.length() == 0) 2469 { 2470 return null; 2471 } 2472 2473 if (!"dn:".equalsIgnoreCase(authzID.toString())) 2474 { 2475 return authzID; 2476 } 2477 return null; 2478 } 2479}