001/*
002 * Copyright 2007-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 UnboundID Corp.
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 (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  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               concatenateStrings("{", " '", ",", "'", " }", rcc.getChoices());
542          unhandledCallbackMessages.add(
543               ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get(
544                    rcc.getPrompt(), choices));
545        }
546        else
547        {
548          final String[] choices = rcc.getChoices();
549          for (int i=0; i < choices.length; i++)
550          {
551            if (choices[i].equals(realm))
552            {
553              rcc.setSelectedIndex(i);
554              break;
555            }
556          }
557        }
558      }
559      else
560      {
561        // This is an unexpected callback.
562        if (debugEnabled(DebugType.LDAP))
563        {
564          debug(Level.WARNING, DebugType.LDAP,
565               "Unexpected DIGEST-MD5 SASL callback of type " +
566                    callback.getClass().getName());
567        }
568
569        unhandledCallbackMessages.add(ERR_DIGESTMD5_UNEXPECTED_CALLBACK.get(
570             callback.getClass().getName()));
571      }
572    }
573  }
574
575
576
577  /**
578   * {@inheritDoc}
579   */
580  @Override()
581  public int getLastMessageID()
582  {
583    return messageID;
584  }
585
586
587
588  /**
589   * {@inheritDoc}
590   */
591  @Override()
592  public DIGESTMD5BindRequest duplicate()
593  {
594    return duplicate(getControls());
595  }
596
597
598
599  /**
600   * {@inheritDoc}
601   */
602  @Override()
603  public DIGESTMD5BindRequest duplicate(final Control[] controls)
604  {
605    final DIGESTMD5BindRequestProperties properties =
606         new DIGESTMD5BindRequestProperties(authenticationID, password);
607    properties.setAuthorizationID(authorizationID);
608    properties.setRealm(realm);
609    properties.setAllowedQoP(allowedQoP);
610
611    final DIGESTMD5BindRequest bindRequest =
612         new DIGESTMD5BindRequest(properties, controls);
613    bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
614    return bindRequest;
615  }
616
617
618
619  /**
620   * {@inheritDoc}
621   */
622  @Override()
623  public void toString(final StringBuilder buffer)
624  {
625    buffer.append("DIGESTMD5BindRequest(authenticationID='");
626    buffer.append(authenticationID);
627    buffer.append('\'');
628
629    if (authorizationID != null)
630    {
631      buffer.append(", authorizationID='");
632      buffer.append(authorizationID);
633      buffer.append('\'');
634    }
635
636    if (realm != null)
637    {
638      buffer.append(", realm='");
639      buffer.append(realm);
640      buffer.append('\'');
641    }
642
643    buffer.append(", qop='");
644    buffer.append(SASLQualityOfProtection.toString(allowedQoP));
645    buffer.append('\'');
646
647    final Control[] controls = getControls();
648    if (controls.length > 0)
649    {
650      buffer.append(", controls={");
651      for (int i=0; i < controls.length; i++)
652      {
653        if (i > 0)
654        {
655          buffer.append(", ");
656        }
657
658        buffer.append(controls[i]);
659      }
660      buffer.append('}');
661    }
662
663    buffer.append(')');
664  }
665}