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}