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}