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.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 (final 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 @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 concatenateStrings("{", " '", ",", "'", " }", 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 (debugEnabled(DebugType.LDAP)) 564 { 565 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<String>(); 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 = 710 new ArrayList<ToCodeArgHelper>(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}