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