001/* 002 * Copyright 2007-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.List; 030import java.util.logging.Level; 031import javax.security.auth.callback.Callback; 032import javax.security.auth.callback.CallbackHandler; 033import javax.security.auth.callback.NameCallback; 034import javax.security.auth.callback.PasswordCallback; 035import javax.security.sasl.RealmCallback; 036import javax.security.sasl.RealmChoiceCallback; 037import javax.security.sasl.Sasl; 038import javax.security.sasl.SaslClient; 039 040import com.unboundid.asn1.ASN1OctetString; 041import com.unboundid.util.DebugType; 042import com.unboundid.util.InternalUseOnly; 043import com.unboundid.util.NotMutable; 044import com.unboundid.util.ThreadSafety; 045import com.unboundid.util.ThreadSafetyLevel; 046 047import static com.unboundid.ldap.sdk.LDAPMessages.*; 048import static com.unboundid.util.Debug.*; 049import static com.unboundid.util.StaticUtils.*; 050import static com.unboundid.util.Validator.*; 051 052 053 054/** 055 * This class provides a SASL DIGEST-MD5 bind request implementation as 056 * described in <A HREF="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</A>. The 057 * DIGEST-MD5 mechanism can be used to authenticate over an insecure channel 058 * without exposing the credentials (although it requires that the server have 059 * access to the clear-text password). It is similar to CRAM-MD5, but provides 060 * better security by combining random data from both the client and the server, 061 * and allows for greater security and functionality, including the ability to 062 * specify an alternate authorization identity and the ability to use data 063 * integrity or confidentiality protection. 064 * <BR><BR> 065 * Elements included in a DIGEST-MD5 bind request include: 066 * <UL> 067 * <LI>Authentication ID -- A string which identifies the user that is 068 * attempting to authenticate. It should be an "authzId" value as 069 * described in section 5.2.1.8 of 070 * <A HREF="http://www.ietf.org/rfc/rfc4513.txt">RFC 4513</A>. That is, 071 * it should be either "dn:" followed by the distinguished name of the 072 * target user, or "u:" followed by the username. If the "u:" form is 073 * used, then the mechanism used to resolve the provided username to an 074 * entry may vary from server to server.</LI> 075 * <LI>Authorization ID -- An optional string which specifies an alternate 076 * authorization identity that should be used for subsequent operations 077 * requested on the connection. Like the authentication ID, the 078 * authorization ID should use the "authzId" syntax.</LI> 079 * <LI>Realm -- An optional string which specifies the realm into which the 080 * user should authenticate.</LI> 081 * <LI>Password -- The clear-text password for the target user.</LI> 082 * </UL> 083 * <H2>Example</H2> 084 * The following example demonstrates the process for performing a DIGEST-MD5 085 * bind against a directory server with a username of "john.doe" and a password 086 * of "password": 087 * <PRE> 088 * DIGESTMD5BindRequest bindRequest = 089 * new DIGESTMD5BindRequest("u:john.doe", "password"); 090 * BindResult bindResult; 091 * try 092 * { 093 * bindResult = connection.bind(bindRequest); 094 * // If we get here, then the bind was successful. 095 * } 096 * catch (LDAPException le) 097 * { 098 * // The bind failed for some reason. 099 * bindResult = new BindResult(le.toLDAPResult()); 100 * ResultCode resultCode = le.getResultCode(); 101 * String errorMessageFromServer = le.getDiagnosticMessage(); 102 * } 103 * </PRE> 104 */ 105@NotMutable() 106@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 107public final class DIGESTMD5BindRequest 108 extends SASLBindRequest 109 implements CallbackHandler 110{ 111 /** 112 * The name for the DIGEST-MD5 SASL mechanism. 113 */ 114 public static final String DIGESTMD5_MECHANISM_NAME = "DIGEST-MD5"; 115 116 117 118 /** 119 * The serial version UID for this serializable class. 120 */ 121 private static final long serialVersionUID = 867592367640540593L; 122 123 124 125 // The password for this bind request. 126 private final ASN1OctetString password; 127 128 // The message ID from the last LDAP message sent from this request. 129 private int messageID = -1; 130 131 // The SASL quality of protection value(s) allowed for the DIGEST-MD5 bind 132 // request. 133 private final List<SASLQualityOfProtection> allowedQoP; 134 135 // A list that will be updated with messages about any unhandled callbacks 136 // encountered during processing. 137 private final List<String> unhandledCallbackMessages; 138 139 // The authentication ID string for this bind request. 140 private final String authenticationID; 141 142 // The authorization ID string for this bind request, if available. 143 private final String authorizationID; 144 145 // The realm form this bind request, if available. 146 private final String realm; 147 148 149 150 /** 151 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication 152 * ID and password. It will not include an authorization ID, a realm, or any 153 * controls. 154 * 155 * @param authenticationID The authentication ID for this bind request. It 156 * must not be {@code null}. 157 * @param password The password for this bind request. It must not 158 * be {@code null}. 159 */ 160 public DIGESTMD5BindRequest(final String authenticationID, 161 final String password) 162 { 163 this(authenticationID, null, new ASN1OctetString(password), null, 164 NO_CONTROLS); 165 166 ensureNotNull(password); 167 } 168 169 170 171 /** 172 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication 173 * ID and password. It will not include an authorization ID, a realm, or any 174 * controls. 175 * 176 * @param authenticationID The authentication ID for this bind request. It 177 * must not be {@code null}. 178 * @param password The password for this bind request. It must not 179 * be {@code null}. 180 */ 181 public DIGESTMD5BindRequest(final String authenticationID, 182 final byte[] password) 183 { 184 this(authenticationID, null, new ASN1OctetString(password), null, 185 NO_CONTROLS); 186 187 ensureNotNull(password); 188 } 189 190 191 192 /** 193 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication 194 * ID and password. It will not include an authorization ID, a realm, or any 195 * controls. 196 * 197 * @param authenticationID The authentication ID for this bind request. It 198 * must not be {@code null}. 199 * @param password The password for this bind request. It must not 200 * be {@code null}. 201 */ 202 public DIGESTMD5BindRequest(final String authenticationID, 203 final ASN1OctetString password) 204 { 205 this(authenticationID, null, password, null, NO_CONTROLS); 206 } 207 208 209 210 /** 211 * Creates a new SASL DIGEST-MD5 bind request with the provided information. 212 * 213 * @param authenticationID The authentication ID for this bind request. It 214 * must not be {@code null}. 215 * @param authorizationID The authorization ID for this bind request. It 216 * may be {@code null} if there will not be an 217 * alternate authorization identity. 218 * @param password The password for this bind request. It must not 219 * be {@code null}. 220 * @param realm The realm to use for the authentication. It may 221 * be {@code null} if the server supports a default 222 * realm. 223 * @param controls The set of controls to include in the request. 224 */ 225 public DIGESTMD5BindRequest(final String authenticationID, 226 final String authorizationID, 227 final String password, final String realm, 228 final Control... controls) 229 { 230 this(authenticationID, authorizationID, new ASN1OctetString(password), 231 realm, controls); 232 233 ensureNotNull(password); 234 } 235 236 237 238 /** 239 * Creates a new SASL DIGEST-MD5 bind request with the provided information. 240 * 241 * @param authenticationID The authentication ID for this bind request. It 242 * must not be {@code null}. 243 * @param authorizationID The authorization ID for this bind request. It 244 * may be {@code null} if there will not be an 245 * alternate authorization identity. 246 * @param password The password for this bind request. It must not 247 * be {@code null}. 248 * @param realm The realm to use for the authentication. It may 249 * be {@code null} if the server supports a default 250 * realm. 251 * @param controls The set of controls to include in the request. 252 */ 253 public DIGESTMD5BindRequest(final String authenticationID, 254 final String authorizationID, 255 final byte[] password, final String realm, 256 final Control... controls) 257 { 258 this(authenticationID, authorizationID, new ASN1OctetString(password), 259 realm, controls); 260 261 ensureNotNull(password); 262 } 263 264 265 266 /** 267 * Creates a new SASL DIGEST-MD5 bind request with the provided information. 268 * 269 * @param authenticationID The authentication ID for this bind request. It 270 * must not be {@code null}. 271 * @param authorizationID The authorization ID for this bind request. It 272 * may be {@code null} if there will not be an 273 * alternate authorization identity. 274 * @param password The password for this bind request. It must not 275 * be {@code null}. 276 * @param realm The realm to use for the authentication. It may 277 * be {@code null} if the server supports a default 278 * realm. 279 * @param controls The set of controls to include in the request. 280 */ 281 public DIGESTMD5BindRequest(final String authenticationID, 282 final String authorizationID, 283 final ASN1OctetString password, 284 final String realm, final Control... controls) 285 { 286 super(controls); 287 288 ensureNotNull(authenticationID, password); 289 290 this.authenticationID = authenticationID; 291 this.authorizationID = authorizationID; 292 this.password = password; 293 this.realm = realm; 294 295 allowedQoP = Collections.unmodifiableList( 296 Arrays.asList(SASLQualityOfProtection.AUTH)); 297 298 unhandledCallbackMessages = new ArrayList<String>(5); 299 } 300 301 302 303 /** 304 * Creates a new SASL DIGEST-MD5 bind request with the provided set of 305 * properties. 306 * 307 * @param properties The properties to use for this 308 * @param controls The set of controls to include in the request. 309 */ 310 public DIGESTMD5BindRequest(final DIGESTMD5BindRequestProperties properties, 311 final Control... controls) 312 { 313 super(controls); 314 315 ensureNotNull(properties); 316 317 authenticationID = properties.getAuthenticationID(); 318 authorizationID = properties.getAuthorizationID(); 319 password = properties.getPassword(); 320 realm = properties.getRealm(); 321 allowedQoP = properties.getAllowedQoP(); 322 323 unhandledCallbackMessages = new ArrayList<String>(5); 324 } 325 326 327 328 /** 329 * {@inheritDoc} 330 */ 331 @Override() 332 public String getSASLMechanismName() 333 { 334 return DIGESTMD5_MECHANISM_NAME; 335 } 336 337 338 339 /** 340 * Retrieves the authentication ID for this bind request. 341 * 342 * @return The authentication ID for this bind request. 343 */ 344 public String getAuthenticationID() 345 { 346 return authenticationID; 347 } 348 349 350 351 /** 352 * Retrieves the authorization ID for this bind request, if any. 353 * 354 * @return The authorization ID for this bind request, or {@code null} if 355 * there should not be a separate authorization identity. 356 */ 357 public String getAuthorizationID() 358 { 359 return authorizationID; 360 } 361 362 363 364 /** 365 * Retrieves the string representation of the password for this bind request. 366 * 367 * @return The string representation of the password for this bind request. 368 */ 369 public String getPasswordString() 370 { 371 return password.stringValue(); 372 } 373 374 375 376 /** 377 * Retrieves the bytes that comprise the the password for this bind request. 378 * 379 * @return The bytes that comprise the password for this bind request. 380 */ 381 public byte[] getPasswordBytes() 382 { 383 return password.getValue(); 384 } 385 386 387 388 /** 389 * Retrieves the realm for this bind request, if any. 390 * 391 * @return The realm for this bind request, or {@code null} if none was 392 * defined and the server should use the default realm. 393 */ 394 public String getRealm() 395 { 396 return realm; 397 } 398 399 400 401 /** 402 * Retrieves the list of allowed qualities of protection that may be used for 403 * communication that occurs on the connection after the authentication has 404 * completed, in order from most preferred to least preferred. 405 * 406 * @return The list of allowed qualities of protection that may be used for 407 * communication that occurs on the connection after the 408 * authentication has completed, in order from most preferred to 409 * least preferred. 410 */ 411 public List<SASLQualityOfProtection> getAllowedQoP() 412 { 413 return allowedQoP; 414 } 415 416 417 418 /** 419 * Sends this bind request to the target server over the provided connection 420 * and returns the corresponding response. 421 * 422 * @param connection The connection to use to send this bind request to the 423 * server and read the associated response. 424 * @param depth The current referral depth for this request. It should 425 * always be one for the initial request, and should only 426 * be incremented when following referrals. 427 * 428 * @return The bind response read from the server. 429 * 430 * @throws LDAPException If a problem occurs while sending the request or 431 * reading the response. 432 */ 433 @Override() 434 protected BindResult process(final LDAPConnection connection, final int depth) 435 throws LDAPException 436 { 437 unhandledCallbackMessages.clear(); 438 439 final String[] mechanisms = { DIGESTMD5_MECHANISM_NAME }; 440 441 final HashMap<String,Object> saslProperties = new HashMap<String,Object>(); 442 saslProperties.put(Sasl.QOP, SASLQualityOfProtection.toString(allowedQoP)); 443 saslProperties.put(Sasl.SERVER_AUTH, "false"); 444 445 final SaslClient saslClient; 446 try 447 { 448 saslClient = Sasl.createSaslClient(mechanisms, authorizationID, "ldap", 449 connection.getConnectedAddress(), 450 saslProperties, this); 451 } 452 catch (Exception e) 453 { 454 debugException(e); 455 throw new LDAPException(ResultCode.LOCAL_ERROR, 456 ERR_DIGESTMD5_CANNOT_CREATE_SASL_CLIENT.get(getExceptionMessage(e)), 457 e); 458 } 459 460 final SASLHelper helper = new SASLHelper(this, connection, 461 DIGESTMD5_MECHANISM_NAME, saslClient, getControls(), 462 getResponseTimeoutMillis(connection), unhandledCallbackMessages); 463 464 try 465 { 466 return helper.processSASLBind(); 467 } 468 finally 469 { 470 messageID = helper.getMessageID(); 471 } 472 } 473 474 475 476 /** 477 * {@inheritDoc} 478 */ 479 @Override() 480 public DIGESTMD5BindRequest getRebindRequest(final String host, 481 final int port) 482 { 483 final DIGESTMD5BindRequestProperties properties = 484 new DIGESTMD5BindRequestProperties(authenticationID, password); 485 properties.setAuthorizationID(authorizationID); 486 properties.setRealm(realm); 487 properties.setAllowedQoP(allowedQoP); 488 489 return new DIGESTMD5BindRequest(properties, getControls()); 490 } 491 492 493 494 /** 495 * Handles any necessary callbacks required for SASL authentication. 496 * 497 * @param callbacks The set of callbacks to be handled. 498 */ 499 @InternalUseOnly() 500 public void handle(final Callback[] callbacks) 501 { 502 for (final Callback callback : callbacks) 503 { 504 if (callback instanceof NameCallback) 505 { 506 ((NameCallback) callback).setName(authenticationID); 507 } 508 else if (callback instanceof PasswordCallback) 509 { 510 ((PasswordCallback) callback).setPassword( 511 password.stringValue().toCharArray()); 512 } 513 else if (callback instanceof RealmCallback) 514 { 515 final RealmCallback rc = (RealmCallback) callback; 516 if (realm == null) 517 { 518 final String defaultRealm = rc.getDefaultText(); 519 if (defaultRealm == null) 520 { 521 unhandledCallbackMessages.add( 522 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get( 523 String.valueOf(rc.getPrompt()))); 524 } 525 else 526 { 527 rc.setText(defaultRealm); 528 } 529 } 530 else 531 { 532 rc.setText(realm); 533 } 534 } 535 else if (callback instanceof RealmChoiceCallback) 536 { 537 final RealmChoiceCallback rcc = (RealmChoiceCallback) callback; 538 if (realm == null) 539 { 540 final String choices = 541 concatenateStrings("{", " '", ",", "'", " }", rcc.getChoices()); 542 unhandledCallbackMessages.add( 543 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get( 544 rcc.getPrompt(), choices)); 545 } 546 else 547 { 548 final String[] choices = rcc.getChoices(); 549 for (int i=0; i < choices.length; i++) 550 { 551 if (choices[i].equals(realm)) 552 { 553 rcc.setSelectedIndex(i); 554 break; 555 } 556 } 557 } 558 } 559 else 560 { 561 // This is an unexpected callback. 562 if (debugEnabled(DebugType.LDAP)) 563 { 564 debug(Level.WARNING, DebugType.LDAP, 565 "Unexpected DIGEST-MD5 SASL callback of type " + 566 callback.getClass().getName()); 567 } 568 569 unhandledCallbackMessages.add(ERR_DIGESTMD5_UNEXPECTED_CALLBACK.get( 570 callback.getClass().getName())); 571 } 572 } 573 } 574 575 576 577 /** 578 * {@inheritDoc} 579 */ 580 @Override() 581 public int getLastMessageID() 582 { 583 return messageID; 584 } 585 586 587 588 /** 589 * {@inheritDoc} 590 */ 591 @Override() 592 public DIGESTMD5BindRequest duplicate() 593 { 594 return duplicate(getControls()); 595 } 596 597 598 599 /** 600 * {@inheritDoc} 601 */ 602 @Override() 603 public DIGESTMD5BindRequest duplicate(final Control[] controls) 604 { 605 final DIGESTMD5BindRequestProperties properties = 606 new DIGESTMD5BindRequestProperties(authenticationID, password); 607 properties.setAuthorizationID(authorizationID); 608 properties.setRealm(realm); 609 properties.setAllowedQoP(allowedQoP); 610 611 final DIGESTMD5BindRequest bindRequest = 612 new DIGESTMD5BindRequest(properties, controls); 613 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 614 return bindRequest; 615 } 616 617 618 619 /** 620 * {@inheritDoc} 621 */ 622 @Override() 623 public void toString(final StringBuilder buffer) 624 { 625 buffer.append("DIGESTMD5BindRequest(authenticationID='"); 626 buffer.append(authenticationID); 627 buffer.append('\''); 628 629 if (authorizationID != null) 630 { 631 buffer.append(", authorizationID='"); 632 buffer.append(authorizationID); 633 buffer.append('\''); 634 } 635 636 if (realm != null) 637 { 638 buffer.append(", realm='"); 639 buffer.append(realm); 640 buffer.append('\''); 641 } 642 643 buffer.append(", qop='"); 644 buffer.append(SASLQualityOfProtection.toString(allowedQoP)); 645 buffer.append('\''); 646 647 final Control[] controls = getControls(); 648 if (controls.length > 0) 649 { 650 buffer.append(", controls={"); 651 for (int i=0; i < controls.length; i++) 652 { 653 if (i > 0) 654 { 655 buffer.append(", "); 656 } 657 658 buffer.append(controls[i]); 659 } 660 buffer.append('}'); 661 } 662 663 buffer.append(')'); 664 } 665 666 667 668 /** 669 * {@inheritDoc} 670 */ 671 @Override() 672 public void toCode(final List<String> lineList, final String requestID, 673 final int indentSpaces, final boolean includeProcessing) 674 { 675 // Create and update the bind request properties object. 676 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 677 "DIGESTMD5BindRequestProperties", 678 requestID + "RequestProperties", 679 "new DIGESTMD5BindRequestProperties", 680 ToCodeArgHelper.createString(authenticationID, "Authentication ID"), 681 ToCodeArgHelper.createString("---redacted-password---", "Password")); 682 683 if (authorizationID != null) 684 { 685 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 686 requestID + "RequestProperties.setAuthorizationID", 687 ToCodeArgHelper.createString(authorizationID, null)); 688 } 689 690 if (realm != null) 691 { 692 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 693 requestID + "RequestProperties.setRealm", 694 ToCodeArgHelper.createString(realm, null)); 695 } 696 697 final ArrayList<String> qopValues = new ArrayList<String>(); 698 for (final SASLQualityOfProtection qop : allowedQoP) 699 { 700 qopValues.add("SASLQualityOfProtection." + qop.name()); 701 } 702 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 703 requestID + "RequestProperties.setAllowedQoP", 704 ToCodeArgHelper.createRaw(qopValues, null)); 705 706 707 // Create the request variable. 708 final ArrayList<ToCodeArgHelper> constructorArgs = 709 new ArrayList<ToCodeArgHelper>(2); 710 constructorArgs.add( 711 ToCodeArgHelper.createRaw(requestID + "RequestProperties", null)); 712 713 final Control[] controls = getControls(); 714 if (controls.length > 0) 715 { 716 constructorArgs.add(ToCodeArgHelper.createControlArray(controls, 717 "Bind Controls")); 718 } 719 720 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 721 "DIGESTMD5BindRequest", requestID + "Request", 722 "new DIGESTMD5BindRequest", constructorArgs); 723 724 725 // Add lines for processing the request and obtaining the result. 726 if (includeProcessing) 727 { 728 // Generate a string with the appropriate indent. 729 final StringBuilder buffer = new StringBuilder(); 730 for (int i=0; i < indentSpaces; i++) 731 { 732 buffer.append(' '); 733 } 734 final String indent = buffer.toString(); 735 736 lineList.add(""); 737 lineList.add(indent + "try"); 738 lineList.add(indent + '{'); 739 lineList.add(indent + " BindResult " + requestID + 740 "Result = connection.bind(" + requestID + "Request);"); 741 lineList.add(indent + " // The bind was processed successfully."); 742 lineList.add(indent + '}'); 743 lineList.add(indent + "catch (LDAPException e)"); 744 lineList.add(indent + '{'); 745 lineList.add(indent + " // The bind failed. Maybe the following will " + 746 "help explain why."); 747 lineList.add(indent + " // Note that the connection is now likely in " + 748 "an unauthenticated state."); 749 lineList.add(indent + " ResultCode resultCode = e.getResultCode();"); 750 lineList.add(indent + " String message = e.getMessage();"); 751 lineList.add(indent + " String matchedDN = e.getMatchedDN();"); 752 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();"); 753 lineList.add(indent + " Control[] responseControls = " + 754 "e.getResponseControls();"); 755 lineList.add(indent + '}'); 756 } 757 } 758}