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