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