001/* 002 * Copyright 2007-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 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 = new HashMap<>(20); 439 saslProperties.put(Sasl.QOP, SASLQualityOfProtection.toString(allowedQoP)); 440 saslProperties.put(Sasl.SERVER_AUTH, "false"); 441 442 final SaslClient saslClient; 443 try 444 { 445 final String[] mechanisms = { DIGESTMD5_MECHANISM_NAME }; 446 saslClient = Sasl.createSaslClient(mechanisms, authorizationID, "ldap", 447 connection.getConnectedAddress(), 448 saslProperties, this); 449 } 450 catch (final Exception e) 451 { 452 Debug.debugException(e); 453 throw new LDAPException(ResultCode.LOCAL_ERROR, 454 ERR_DIGESTMD5_CANNOT_CREATE_SASL_CLIENT.get( 455 StaticUtils.getExceptionMessage(e)), 456 e); 457 } 458 459 final SASLHelper helper = new SASLHelper(this, connection, 460 DIGESTMD5_MECHANISM_NAME, saslClient, getControls(), 461 getResponseTimeoutMillis(connection), unhandledCallbackMessages); 462 463 try 464 { 465 return helper.processSASLBind(); 466 } 467 finally 468 { 469 messageID = helper.getMessageID(); 470 } 471 } 472 473 474 475 /** 476 * {@inheritDoc} 477 */ 478 @Override() 479 public DIGESTMD5BindRequest getRebindRequest(final String host, 480 final int port) 481 { 482 final DIGESTMD5BindRequestProperties properties = 483 new DIGESTMD5BindRequestProperties(authenticationID, password); 484 properties.setAuthorizationID(authorizationID); 485 properties.setRealm(realm); 486 properties.setAllowedQoP(allowedQoP); 487 488 return new DIGESTMD5BindRequest(properties, getControls()); 489 } 490 491 492 493 /** 494 * Handles any necessary callbacks required for SASL authentication. 495 * 496 * @param callbacks The set of callbacks to be handled. 497 */ 498 @InternalUseOnly() 499 @Override() 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 StaticUtils.concatenateStrings("{", " '", ",", "'", " }", 542 rcc.getChoices()); 543 unhandledCallbackMessages.add( 544 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get( 545 rcc.getPrompt(), choices)); 546 } 547 else 548 { 549 final String[] choices = rcc.getChoices(); 550 for (int i=0; i < choices.length; i++) 551 { 552 if (choices[i].equals(realm)) 553 { 554 rcc.setSelectedIndex(i); 555 break; 556 } 557 } 558 } 559 } 560 else 561 { 562 // This is an unexpected callback. 563 if (Debug.debugEnabled(DebugType.LDAP)) 564 { 565 Debug.debug(Level.WARNING, DebugType.LDAP, 566 "Unexpected DIGEST-MD5 SASL callback of type " + 567 callback.getClass().getName()); 568 } 569 570 unhandledCallbackMessages.add(ERR_DIGESTMD5_UNEXPECTED_CALLBACK.get( 571 callback.getClass().getName())); 572 } 573 } 574 } 575 576 577 578 /** 579 * {@inheritDoc} 580 */ 581 @Override() 582 public int getLastMessageID() 583 { 584 return messageID; 585 } 586 587 588 589 /** 590 * {@inheritDoc} 591 */ 592 @Override() 593 public DIGESTMD5BindRequest duplicate() 594 { 595 return duplicate(getControls()); 596 } 597 598 599 600 /** 601 * {@inheritDoc} 602 */ 603 @Override() 604 public DIGESTMD5BindRequest duplicate(final Control[] controls) 605 { 606 final DIGESTMD5BindRequestProperties properties = 607 new DIGESTMD5BindRequestProperties(authenticationID, password); 608 properties.setAuthorizationID(authorizationID); 609 properties.setRealm(realm); 610 properties.setAllowedQoP(allowedQoP); 611 612 final DIGESTMD5BindRequest bindRequest = 613 new DIGESTMD5BindRequest(properties, controls); 614 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 615 return bindRequest; 616 } 617 618 619 620 /** 621 * {@inheritDoc} 622 */ 623 @Override() 624 public void toString(final StringBuilder buffer) 625 { 626 buffer.append("DIGESTMD5BindRequest(authenticationID='"); 627 buffer.append(authenticationID); 628 buffer.append('\''); 629 630 if (authorizationID != null) 631 { 632 buffer.append(", authorizationID='"); 633 buffer.append(authorizationID); 634 buffer.append('\''); 635 } 636 637 if (realm != null) 638 { 639 buffer.append(", realm='"); 640 buffer.append(realm); 641 buffer.append('\''); 642 } 643 644 buffer.append(", qop='"); 645 buffer.append(SASLQualityOfProtection.toString(allowedQoP)); 646 buffer.append('\''); 647 648 final Control[] controls = getControls(); 649 if (controls.length > 0) 650 { 651 buffer.append(", controls={"); 652 for (int i=0; i < controls.length; i++) 653 { 654 if (i > 0) 655 { 656 buffer.append(", "); 657 } 658 659 buffer.append(controls[i]); 660 } 661 buffer.append('}'); 662 } 663 664 buffer.append(')'); 665 } 666 667 668 669 /** 670 * {@inheritDoc} 671 */ 672 @Override() 673 public void toCode(final List<String> lineList, final String requestID, 674 final int indentSpaces, final boolean includeProcessing) 675 { 676 // Create and update the bind request properties object. 677 ToCodeHelper.generateMethodCall(lineList, indentSpaces, 678 "DIGESTMD5BindRequestProperties", 679 requestID + "RequestProperties", 680 "new DIGESTMD5BindRequestProperties", 681 ToCodeArgHelper.createString(authenticationID, "Authentication ID"), 682 ToCodeArgHelper.createString("---redacted-password---", "Password")); 683 684 if (authorizationID != null) 685 { 686 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 687 requestID + "RequestProperties.setAuthorizationID", 688 ToCodeArgHelper.createString(authorizationID, null)); 689 } 690 691 if (realm != null) 692 { 693 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 694 requestID + "RequestProperties.setRealm", 695 ToCodeArgHelper.createString(realm, null)); 696 } 697 698 final ArrayList<String> qopValues = new ArrayList<>(3); 699 for (final SASLQualityOfProtection qop : allowedQoP) 700 { 701 qopValues.add("SASLQualityOfProtection." + qop.name()); 702 } 703 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null, 704 requestID + "RequestProperties.setAllowedQoP", 705 ToCodeArgHelper.createRaw(qopValues, null)); 706 707 708 // Create the request variable. 709 final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(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}