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