001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.tools;
018
019import static com.forgerock.opendj.cli.ArgumentConstants.*;
020
021import static org.opends.messages.ToolMessages.*;
022import static org.opends.server.protocols.ldap.LDAPConstants.*;
023import static org.opends.server.util.ServerConstants.*;
024import static org.opends.server.util.StaticUtils.*;
025
026import java.io.BufferedWriter;
027import java.io.File;
028import java.io.FileWriter;
029import java.io.IOException;
030import java.io.UnsupportedEncodingException;
031import java.security.MessageDigest;
032import java.security.PrivilegedExceptionAction;
033import java.security.SecureRandom;
034import java.util.Arrays;
035import java.util.HashMap;
036import java.util.Iterator;
037import java.util.LinkedHashMap;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Map;
041import java.util.Map.Entry;
042import java.util.StringTokenizer;
043import java.util.concurrent.atomic.AtomicInteger;
044
045import javax.security.auth.Subject;
046import javax.security.auth.callback.Callback;
047import javax.security.auth.callback.CallbackHandler;
048import javax.security.auth.callback.NameCallback;
049import javax.security.auth.callback.PasswordCallback;
050import javax.security.auth.callback.UnsupportedCallbackException;
051import javax.security.auth.login.LoginContext;
052import javax.security.sasl.Sasl;
053import javax.security.sasl.SaslClient;
054
055import org.forgerock.i18n.LocalizableMessage;
056import org.forgerock.i18n.LocalizableMessageDescriptor.Arg0;
057import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
058import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
059import org.forgerock.opendj.ldap.ByteSequence;
060import org.forgerock.opendj.ldap.ByteString;
061import org.forgerock.opendj.ldap.DecodeException;
062import org.opends.server.protocols.ldap.BindRequestProtocolOp;
063import org.opends.server.protocols.ldap.BindResponseProtocolOp;
064import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp;
065import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp;
066import org.opends.server.protocols.ldap.LDAPMessage;
067import org.opends.server.types.Control;
068import org.opends.server.types.LDAPException;
069import org.opends.server.util.Base64;
070
071import com.forgerock.opendj.cli.ClientException;
072import com.forgerock.opendj.cli.ConsoleApplication;
073import com.forgerock.opendj.cli.ReturnCode;
074
075/**
076 * This class provides a generic interface that LDAP clients can use to perform
077 * various kinds of authentication to the Directory Server.  This handles both
078 * simple authentication as well as several SASL mechanisms including:
079 * <UL>
080 *   <LI>ANONYMOUS</LI>
081 *   <LI>CRAM-MD5</LI>
082 *   <LI>DIGEST-MD5</LI>
083 *   <LI>EXTERNAL</LI>
084 *   <LI>GSSAPI</LI>
085 *   <LI>PLAIN</LI>
086 * </UL>
087 * <BR><BR>
088 * Note that this implementation is not thread safe, so if the same
089 * <CODE>AuthenticationHandler</CODE> object is to be used concurrently by
090 * multiple threads, it must be externally synchronized.
091 */
092public class LDAPAuthenticationHandler
093       implements PrivilegedExceptionAction<Object>, CallbackHandler
094{
095  /** The LDAP reader that will be used to read data from the server. */
096  private final LDAPReader reader;
097  /** The LDAP writer that will be used to send data to the server. */
098  private final LDAPWriter writer;
099
100  /** The atomic integer that will be used to obtain message IDs for request messages. */
101  private final AtomicInteger nextMessageID;
102
103  /** An array filled with the inner pad byte. */
104  private byte[] iPad;
105  /** An array filled with the outer pad byte. */
106  private byte[] oPad;
107
108  /** The message digest that will be used to create MD5 hashes. */
109  private MessageDigest md5Digest;
110  /** The secure random number generator for use by this authentication handler. */
111  private SecureRandom secureRandom;
112
113  /** The bind DN for GSSAPI authentication. */
114  private ByteSequence gssapiBindDN;
115  /** The authentication ID for GSSAPI authentication. */
116  private String gssapiAuthID;
117  /** The authorization ID for GSSAPI authentication. */
118  private String gssapiAuthzID;
119  /** The authentication password for GSSAPI authentication. */
120  private char[] gssapiAuthPW;
121  /** The quality of protection for GSSAPI authentication. */
122  private String gssapiQoP;
123
124  /** The host name used to connect to the remote system. */
125  private final String hostName;
126
127  /** The SASL mechanism that will be used for callback authentication. */
128  private String saslMechanism;
129
130
131
132  /**
133   * Creates a new instance of this authentication handler.  All initialization
134   * will be done lazily to avoid unnecessary performance hits, particularly
135   * for cases in which simple authentication will be used as it does not
136   * require any particularly expensive processing.
137   *
138   * @param  reader         The LDAP reader that will be used to read data from
139   *                        the server.
140   * @param  writer         The LDAP writer that will be used to send data to
141   *                        the server.
142   * @param  hostName       The host name used to connect to the remote system
143   *                        (fully-qualified if possible).
144   * @param  nextMessageID  The atomic integer that will be used to obtain
145   *                        message IDs for request messages.
146   */
147  public LDAPAuthenticationHandler(LDAPReader reader, LDAPWriter writer,
148                                   String hostName, AtomicInteger nextMessageID)
149  {
150    this.reader = reader;
151    this.writer = writer;
152    this.hostName      = hostName;
153    this.nextMessageID = nextMessageID;
154
155    md5Digest    = null;
156    secureRandom = null;
157    iPad         = null;
158    oPad         = null;
159  }
160
161
162
163  /**
164   * Retrieves a list of the SASL mechanisms that are supported by this client
165   * library.
166   *
167   * @return  A list of the SASL mechanisms that are supported by this client
168   *          library.
169   */
170  public static String[] getSupportedSASLMechanisms()
171  {
172    return new String[]
173    {
174      SASL_MECHANISM_ANONYMOUS,
175      SASL_MECHANISM_CRAM_MD5,
176      SASL_MECHANISM_DIGEST_MD5,
177      SASL_MECHANISM_EXTERNAL,
178      SASL_MECHANISM_GSSAPI,
179      SASL_MECHANISM_PLAIN
180    };
181  }
182
183
184
185  /**
186   * Retrieves a list of the SASL properties that may be provided for the
187   * specified SASL mechanism, mapped from the property names to their
188   * corresponding descriptions.
189   *
190   * @param  mechanism  The name of the SASL mechanism for which to obtain the
191   *                    list of supported properties.
192   *
193   * @return  A list of the SASL properties that may be provided for the
194   *          specified SASL mechanism, mapped from the property names to their
195   *          corresponding descriptions.
196   */
197  public static Map<String, LocalizableMessage> getSASLProperties(String mechanism)
198  {
199    switch (toUpperCase(mechanism))
200    {
201    case SASL_MECHANISM_ANONYMOUS:
202      return getSASLAnonymousProperties();
203    case SASL_MECHANISM_CRAM_MD5:
204      return getSASLCRAMMD5Properties();
205    case SASL_MECHANISM_DIGEST_MD5:
206      return getSASLDigestMD5Properties();
207    case SASL_MECHANISM_EXTERNAL:
208      return getSASLExternalProperties();
209    case SASL_MECHANISM_GSSAPI:
210      return getSASLGSSAPIProperties();
211    case SASL_MECHANISM_PLAIN:
212      return getSASLPlainProperties();
213    default:
214      // This is an unsupported mechanism.
215      return null;
216    }
217  }
218
219
220
221  /**
222   * Processes a bind using simple authentication with the provided information.
223   * If the bind fails, then an exception will be thrown with information about
224   * the reason for the failure.  If the bind is successful but there may be
225   * some special information that the client should be given, then it will be
226   * returned as a String.
227   *
228   * @param  ldapVersion       The LDAP protocol version to use for the bind
229   *                           request.
230   * @param  bindDN            The DN to use to bind to the Directory Server, or
231   *                           <CODE>null</CODE> if it is to be an anonymous
232   *                           bind.
233   * @param  bindPassword      The password to use to bind to the Directory
234   *                           Server, or <CODE>null</CODE> if it is to be an
235   *                           anonymous bind.
236   * @param  requestControls   The set of controls to include the request to the
237   *                           server.
238   * @param  responseControls  A list to hold the set of controls included in
239   *                           the response from the server.
240   *
241   * @return  A message providing additional information about the bind if
242   *          appropriate, or <CODE>null</CODE> if there is no special
243   *          information available.
244   *
245   * @throws  ClientException  If a client-side problem prevents the bind
246   *                           attempt from succeeding.
247   *
248   * @throws  LDAPException  If the bind fails or some other server-side problem
249   *                         occurs during processing.
250   */
251  public String doSimpleBind(int ldapVersion, ByteSequence bindDN,
252                             ByteSequence bindPassword,
253                             List<Control> requestControls,
254                             List<Control> responseControls)
255         throws ClientException, LDAPException
256  {
257    //Password is empty, set it to ByteString.empty.
258    if (bindPassword == null)
259    {
260        bindPassword = ByteString.empty();
261    }
262
263    // Make sure that critical elements aren't null.
264    if (bindDN == null)
265    {
266      bindDN = ByteString.empty();
267    }
268
269    sendSimpleBindRequest(ldapVersion, bindDN, bindPassword, requestControls);
270
271    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
272    responseControls.addAll(responseMessage.getControls());
273    checkConnected(responseMessage);
274    return checkSuccessfulSimpleBind(responseMessage);
275  }
276
277  private void sendSimpleBindRequest(int ldapVersion, ByteSequence bindDN, ByteSequence bindPassword,
278      List<Control> requestControls) throws ClientException
279  {
280    BindRequestProtocolOp bindRequest =
281        new BindRequestProtocolOp(bindDN.toByteString(), ldapVersion, bindPassword.toByteString());
282    LDAPMessage bindRequestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, requestControls);
283
284    try
285    {
286      writer.writeMessage(bindRequestMessage);
287    }
288    catch (IOException ioe)
289    {
290      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(ioe));
291      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
292    }
293    catch (Exception e)
294    {
295      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(e));
296      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
297    }
298  }
299
300  private BindResponseProtocolOp checkSuccessfulBind(LDAPMessage responseMessage, String saslMechanism)
301      throws LDAPException
302  {
303    BindResponseProtocolOp bindResponse = responseMessage.getBindResponseProtocolOp();
304    int resultCode = bindResponse.getResultCode();
305    if (resultCode != ReturnCode.SUCCESS.get())
306    {
307      // FIXME -- Add support for referrals.
308      LocalizableMessage message = ERR_LDAPAUTH_SASL_BIND_FAILED.get(saslMechanism);
309      throw new LDAPException(resultCode, bindResponse.getErrorMessage(), message, bindResponse.getMatchedDN(), null);
310    }
311    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
312    return bindResponse;
313  }
314
315  private String checkSuccessfulSimpleBind(LDAPMessage responseMessage) throws LDAPException
316  {
317    BindResponseProtocolOp bindResponse = responseMessage.getBindResponseProtocolOp();
318    int resultCode = bindResponse.getResultCode();
319    if (resultCode != ReturnCode.SUCCESS.get())
320    {
321      // FIXME -- Add support for referrals.
322      LocalizableMessage message = ERR_LDAPAUTH_SIMPLE_BIND_FAILED.get();
323      throw new LDAPException(resultCode, bindResponse.getErrorMessage(), message, bindResponse.getMatchedDN(), null);
324    }
325    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
326    return null;
327  }
328
329  /**
330   * Processes a SASL bind using the provided information.  If the bind fails,
331   * then an exception will be thrown with information about the reason for the
332   * failure.  If the bind is successful but there may be some special
333   * information that the client should be given, then it will be returned as a
334   * String.
335   *
336   * @param  bindDN            The DN to use to bind to the Directory Server, or
337   *                           <CODE>null</CODE> if the authentication identity
338   *                           is to be set through some other means.
339   * @param  bindPassword      The password to use to bind to the Directory
340   *                           Server, or <CODE>null</CODE> if this is not a
341   *                           password-based SASL mechanism.
342   * @param  mechanism         The name of the SASL mechanism to use to
343   *                           authenticate to the Directory Server.
344   * @param  saslProperties    A set of additional properties that may be needed
345   *                           to process the SASL bind.
346   * @param  requestControls   The set of controls to include the request to the
347   *                           server.
348   * @param  responseControls  A list to hold the set of controls included in
349   *                           the response from the server.
350   *
351   * @return  A message providing additional information about the bind if
352   *          appropriate, or <CODE>null</CODE> if there is no special
353   *          information available.
354   *
355   * @throws  ClientException  If a client-side problem prevents the bind
356   *                           attempt from succeeding.
357   *
358   * @throws  LDAPException  If the bind fails or some other server-side problem
359   *                         occurs during processing.
360   */
361  public String doSASLBind(ByteSequence bindDN, ByteSequence bindPassword,
362                           String mechanism,
363                           Map<String,List<String>> saslProperties,
364                           List<Control> requestControls,
365                           List<Control> responseControls)
366         throws ClientException, LDAPException
367  {
368    // Make sure that critical elements aren't null.
369    if (bindDN == null)
370    {
371      bindDN = ByteString.empty();
372    }
373
374    if (mechanism == null || mechanism.length() == 0)
375    {
376      LocalizableMessage message = ERR_LDAPAUTH_NO_SASL_MECHANISM.get();
377      throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
378    }
379
380
381    // Look at the mechanism name and call the appropriate method to process the request.
382    saslMechanism = toUpperCase(mechanism);
383    switch (saslMechanism)
384    {
385    case SASL_MECHANISM_ANONYMOUS:
386      return doSASLAnonymous(bindDN, saslProperties, requestControls, responseControls);
387    case SASL_MECHANISM_CRAM_MD5:
388      return doSASLCRAMMD5(bindDN, bindPassword, saslProperties, requestControls, responseControls);
389    case SASL_MECHANISM_DIGEST_MD5:
390      return doSASLDigestMD5(bindDN, bindPassword, saslProperties, requestControls, responseControls);
391    case SASL_MECHANISM_EXTERNAL:
392      return doSASLExternal(bindDN, saslProperties, requestControls, responseControls);
393    case SASL_MECHANISM_GSSAPI:
394      return doSASLGSSAPI(bindDN, bindPassword, saslProperties, requestControls, responseControls);
395    case SASL_MECHANISM_PLAIN:
396      return doSASLPlain(bindDN, bindPassword, saslProperties, requestControls, responseControls);
397    default:
398      LocalizableMessage message = ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mechanism);
399      throw new ClientException(ReturnCode.CLIENT_SIDE_AUTH_UNKNOWN, message);
400    }
401  }
402
403
404
405  /**
406   * Processes a SASL ANONYMOUS bind with the provided information.
407   *
408   * @param  bindDN            The DN to use to bind to the Directory Server, or
409   *                           <CODE>null</CODE> if the authentication identity
410   *                           is to be set through some other means.
411   * @param  saslProperties    A set of additional properties that may be needed
412   *                           to process the SASL bind.
413   * @param  requestControls   The set of controls to include the request to the
414   *                           server.
415   * @param  responseControls  A list to hold the set of controls included in
416   *                           the response from the server.
417   *
418   * @return  A message providing additional information about the bind if
419   *          appropriate, or <CODE>null</CODE> if there is no special
420   *          information available.
421   *
422   * @throws  ClientException  If a client-side problem prevents the bind
423   *                           attempt from succeeding.
424   *
425   * @throws  LDAPException  If the bind fails or some other server-side problem
426   *                         occurs during processing.
427   */
428  private String doSASLAnonymous(ByteSequence bindDN,
429                     Map<String,List<String>> saslProperties,
430                     List<Control> requestControls,
431                     List<Control> responseControls)
432         throws ClientException, LDAPException
433  {
434    String trace = null;
435
436    // The only allowed property is the trace property, but it is not required.
437    if (saslProperties != null)
438    {
439      for (Entry<String, List<String>> entry : saslProperties.entrySet())
440      {
441        String name = entry.getKey();
442        List<String> values = entry.getValue();
443        if (name.equalsIgnoreCase(SASL_PROPERTY_TRACE))
444        {
445          // This is acceptable, and we'll take any single value.
446          trace = getSingleValue(values, ERR_LDAPAUTH_TRACE_SINGLE_VALUED);
447        }
448        else
449        {
450          LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
451              name, SASL_MECHANISM_ANONYMOUS);
452          throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
453        }
454      }
455    }
456
457    // Construct the bind request and send it to the server.
458    ByteString saslCredentials = trace != null ? ByteString.valueOfUtf8(trace) : null;
459    sendBindRequest(SASL_MECHANISM_ANONYMOUS, bindDN, saslCredentials, requestControls);
460
461    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
462    responseControls.addAll(responseMessage.getControls());
463    checkConnected(responseMessage);
464    checkSuccessfulBind(responseMessage, SASL_MECHANISM_ANONYMOUS);
465    return null;
466  }
467
468  /**
469   * Retrieves the set of properties that a client may provide when performing a
470   * SASL ANONYMOUS bind, mapped from the property names to their corresponding
471   * descriptions.
472   *
473   * @return  The set of properties that a client may provide when performing a
474   *          SASL ANONYMOUS bind, mapped from the property names to their
475   *          corresponding descriptions.
476   */
477  private static LinkedHashMap<String, LocalizableMessage> getSASLAnonymousProperties()
478  {
479    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1);
480
481    properties.put(SASL_PROPERTY_TRACE,
482                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_TRACE.get());
483
484    return properties;
485  }
486
487
488
489  /**
490   * Processes a SASL CRAM-MD5 bind with the provided information.
491   *
492   * @param  bindDN            The DN to use to bind to the Directory Server, or
493   *                           <CODE>null</CODE> if the authentication identity
494   *                           is to be set through some other means.
495   * @param  bindPassword      The password to use to bind to the Directory
496   *                           Server.
497   * @param  saslProperties    A set of additional properties that may be needed
498   *                           to process the SASL bind.
499   * @param  requestControls   The set of controls to include the request to the
500   *                           server.
501   * @param  responseControls  A list to hold the set of controls included in
502   *                           the response from the server.
503   *
504   * @return  A message providing additional information about the bind if
505   *          appropriate, or <CODE>null</CODE> if there is no special
506   *          information available.
507   *
508   * @throws  ClientException  If a client-side problem prevents the bind
509   *                           attempt from succeeding.
510   *
511   * @throws  LDAPException  If the bind fails or some other server-side problem
512   *                         occurs during processing.
513   */
514  private String doSASLCRAMMD5(ByteSequence bindDN,
515                     ByteSequence bindPassword,
516                     Map<String,List<String>> saslProperties,
517                     List<Control> requestControls,
518                     List<Control> responseControls)
519         throws ClientException, LDAPException
520  {
521    String authID  = null;
522
523
524    // Evaluate the properties provided.  The authID is required, no other
525    // properties are allowed.
526    if (saslProperties == null || saslProperties.isEmpty())
527    {
528      LocalizableMessage message =
529          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_CRAM_MD5);
530      throw new ClientException(
531              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
532    }
533
534    for (Entry<String, List<String>> entry : saslProperties.entrySet())
535    {
536      String name = entry.getKey();
537      List<String> values = entry.getValue();
538      String lowerName = toLowerCase(name);
539
540      if (lowerName.equals(SASL_PROPERTY_AUTHID))
541      {
542        authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
543      }
544      else
545      {
546        LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
547            name, SASL_MECHANISM_CRAM_MD5);
548        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
549      }
550    }
551
552
553    // Make sure that the authID was provided.
554    if (authID == null || authID.length() == 0)
555    {
556      LocalizableMessage message =
557          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_CRAM_MD5);
558      throw new ClientException(
559              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
560    }
561
562
563    // Set password to ByteString.empty if the password is null.
564    if (bindPassword == null)
565    {
566        bindPassword = ByteString.empty();
567    }
568
569    sendInitialBindRequest(SASL_MECHANISM_CRAM_MD5, bindDN);
570
571    LDAPMessage responseMessage1 =
572        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE, SASL_MECHANISM_CRAM_MD5);
573    checkConnected(responseMessage1);
574
575    // Make sure that the bind response has the "SASL bind in progress" result code.
576    BindResponseProtocolOp bindResponse1 =
577         responseMessage1.getBindResponseProtocolOp();
578    int resultCode1 = bindResponse1.getResultCode();
579    if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get())
580    {
581      LocalizableMessage errorMessage = bindResponse1.getErrorMessage();
582      if (errorMessage == null)
583      {
584        errorMessage = LocalizableMessage.EMPTY;
585      }
586
587      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
588          get(SASL_MECHANISM_CRAM_MD5, resultCode1,
589              ReturnCode.get(resultCode1), errorMessage);
590      throw new LDAPException(resultCode1, errorMessage, message,
591                              bindResponse1.getMatchedDN(), null);
592    }
593
594
595    // Make sure that the bind response contains SASL credentials with the
596    // challenge to use for the next stage of the bind.
597    ByteString serverChallenge = bindResponse1.getServerSASLCredentials();
598    if (serverChallenge == null)
599    {
600      LocalizableMessage message = ERR_LDAPAUTH_NO_CRAMMD5_SERVER_CREDENTIALS.get();
601      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
602    }
603
604    // Use the provided password and credentials to generate the CRAM-MD5 response.
605    String salsCredentials = authID + ' ' + generateCRAMMD5Digest(bindPassword, serverChallenge);
606    sendSecondBindRequest(SASL_MECHANISM_CRAM_MD5, bindDN, salsCredentials, requestControls);
607
608    LDAPMessage responseMessage2 =
609        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE, SASL_MECHANISM_CRAM_MD5);
610    responseControls.addAll(responseMessage2.getControls());
611    checkConnected(responseMessage2);
612    checkSuccessfulBind(responseMessage2, SASL_MECHANISM_CRAM_MD5);
613    return null;
614  }
615
616  /**
617   * Construct the initial bind request to send to the server. We'll simply indicate the SASL
618   * mechanism we want to use so the server will send us the challenge.
619   */
620  private void sendInitialBindRequest(String saslMechanism, ByteSequence bindDN) throws ClientException
621  {
622    // FIXME -- Should we include request controls in both stages or just the second stage?
623    BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, null);
624    LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
625
626    try
627    {
628      writer.writeMessage(requestMessage);
629    }
630    catch (IOException ioe)
631    {
632      LocalizableMessage message =
633          ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe));
634      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
635    }
636    catch (Exception e)
637    {
638      LocalizableMessage message =
639          ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(saslMechanism, getExceptionMessage(e));
640      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
641    }
642  }
643
644  private LDAPMessage readBindResponse(Arg2<Object, Object> errCannotReadBindResponse, String saslMechanism)
645      throws ClientException
646  {
647    try
648    {
649      LDAPMessage responseMessage = reader.readMessage();
650      if (responseMessage != null)
651      {
652        return responseMessage;
653      }
654      LocalizableMessage message = ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
655      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message);
656    }
657    catch (DecodeException | LDAPException e)
658    {
659      LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(e));
660      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
661    }
662    catch (IOException ioe)
663    {
664      LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(ioe));
665      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
666    }
667    catch (Exception e)
668    {
669      LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(e));
670      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
671    }
672  }
673
674  /**
675   * Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication
676   * with the given information.
677   *
678   * @param  password   The clear-text password to use when generating the
679   *                    digest.
680   * @param  challenge  The server-supplied challenge to use when generating the
681   *                    digest.
682   *
683   * @return  The generated HMAC-MD5 digest for CRAM-MD5 authentication.
684   *
685   * @throws  ClientException  If a problem occurs while attempting to perform
686   *                           the necessary initialization.
687   */
688  private String generateCRAMMD5Digest(ByteSequence password,
689                                       ByteSequence challenge)
690          throws ClientException
691  {
692    // Perform the necessary initialization if it hasn't been done yet.
693    if (md5Digest == null)
694    {
695      try
696      {
697        md5Digest = MessageDigest.getInstance("MD5");
698      }
699      catch (Exception e)
700      {
701        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
702            getExceptionMessage(e));
703        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
704                message, e);
705      }
706    }
707
708    if (iPad == null)
709    {
710      iPad = new byte[HMAC_MD5_BLOCK_LENGTH];
711      oPad = new byte[HMAC_MD5_BLOCK_LENGTH];
712      Arrays.fill(iPad, CRAMMD5_IPAD_BYTE);
713      Arrays.fill(oPad, CRAMMD5_OPAD_BYTE);
714    }
715
716
717    // Get the byte arrays backing the password and challenge.
718    byte[] p = password.toByteArray();
719    byte[] c = challenge.toByteArray();
720
721
722    // If the password is longer than the HMAC-MD5 block length, then use an
723    // MD5 digest of the password rather than the password itself.
724    if (password.length() > HMAC_MD5_BLOCK_LENGTH)
725    {
726      p = md5Digest.digest(p);
727    }
728
729
730    // Create byte arrays with data needed for the hash generation.
731    byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length];
732    System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH);
733    System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length);
734
735    byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH];
736    System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH);
737
738
739    // Iterate through the bytes in the key and XOR them with the iPad and
740    // oPad as appropriate.
741    for (int i=0; i < p.length; i++)
742    {
743      iPadAndData[i] ^= p[i];
744      oPadAndHash[i] ^= p[i];
745    }
746
747
748    // Copy an MD5 digest of the iPad-XORed key and the data into the array to
749    // be hashed.
750    System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash,
751                     HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH);
752
753
754    // Calculate an MD5 digest of the resulting array and get the corresponding
755    // hex string representation.
756    byte[] digestBytes = md5Digest.digest(oPadAndHash);
757
758    StringBuilder hexDigest = new StringBuilder(2*digestBytes.length);
759    for (byte b : digestBytes)
760    {
761      hexDigest.append(byteToLowerHex(b));
762    }
763
764    return hexDigest.toString();
765  }
766
767
768
769  /**
770   * Retrieves the set of properties that a client may provide when performing a
771   * SASL CRAM-MD5 bind, mapped from the property names to their corresponding
772   * descriptions.
773   *
774   * @return  The set of properties that a client may provide when performing a
775   *          SASL CRAM-MD5 bind, mapped from the property names to their
776   *          corresponding descriptions.
777   */
778  private static LinkedHashMap<String, LocalizableMessage> getSASLCRAMMD5Properties()
779  {
780    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1);
781
782    properties.put(SASL_PROPERTY_AUTHID,
783                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
784
785    return properties;
786  }
787
788
789
790  /**
791   * Processes a SASL DIGEST-MD5 bind with the provided information.
792   *
793   * @param  bindDN            The DN to use to bind to the Directory Server, or
794   *                           <CODE>null</CODE> if the authentication identity
795   *                           is to be set through some other means.
796   * @param  bindPassword      The password to use to bind to the Directory
797   *                           Server.
798   * @param  saslProperties    A set of additional properties that may be needed
799   *                           to process the SASL bind.
800   * @param  requestControls   The set of controls to include the request to the
801   *                           server.
802   * @param  responseControls  A list to hold the set of controls included in
803   *                           the response from the server.
804   *
805   * @return  A message providing additional information about the bind if
806   *          appropriate, or <CODE>null</CODE> if there is no special
807   *          information available.
808   *
809   * @throws  ClientException  If a client-side problem prevents the bind
810   *                           attempt from succeeding.
811   *
812   * @throws  LDAPException  If the bind fails or some other server-side problem
813   *                         occurs during processing.
814   */
815  private String doSASLDigestMD5(ByteSequence bindDN,
816                     ByteSequence bindPassword,
817                     Map<String,List<String>> saslProperties,
818                     List<Control> requestControls,
819                     List<Control> responseControls)
820         throws ClientException, LDAPException
821  {
822    String  authID               = null;
823    String  realm                = null;
824    String  qop                  = "auth";
825    String  digestURI            = "ldap/" + hostName;
826    String  authzID              = null;
827    boolean realmSetFromProperty = false;
828
829
830    // Evaluate the properties provided.  The authID is required.  The realm,
831    // QoP, digest URI, and authzID are optional.
832    if (saslProperties == null || saslProperties.isEmpty())
833    {
834      LocalizableMessage message =
835          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_DIGEST_MD5);
836      throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
837    }
838
839    for (Entry<String, List<String>> entry : saslProperties.entrySet())
840    {
841      String name = entry.getKey();
842      List<String> values = entry.getValue();
843      String lowerName = toLowerCase(name);
844
845      if (lowerName.equals(SASL_PROPERTY_AUTHID))
846      {
847        authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
848      }
849      else if (lowerName.equals(SASL_PROPERTY_REALM))
850      {
851        Iterator<String> iterator = values.iterator();
852        if (iterator.hasNext())
853        {
854          realm                = iterator.next();
855          realmSetFromProperty = true;
856
857          if (iterator.hasNext())
858          {
859            LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get();
860            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
861                                      message);
862          }
863        }
864      }
865      else if (lowerName.equals(SASL_PROPERTY_QOP))
866      {
867        Iterator<String> iterator = values.iterator();
868        if (iterator.hasNext())
869        {
870          qop = toLowerCase(iterator.next());
871
872          if (iterator.hasNext())
873          {
874            LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
875            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
876                                      message);
877          }
878
879          if (qop.equals("auth"))
880          {
881            // This is always fine.
882          }
883          else if (qop.equals("auth-int") || qop.equals("auth-conf"))
884          {
885            // FIXME -- Add support for integrity and confidentiality.
886            LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(qop);
887            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
888                                      message);
889          }
890          else
891          {
892            // This is an illegal value.
893            LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_QOP.get(qop);
894            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
895                                      message);
896          }
897        }
898      }
899      else if (lowerName.equals(SASL_PROPERTY_DIGEST_URI))
900      {
901        digestURI = toLowerCase(getSingleValue(values, ERR_LDAPAUTH_DIGEST_URI_SINGLE_VALUED));
902      }
903      else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
904      {
905        authzID = toLowerCase(getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED));
906      }
907      else
908      {
909        LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
910            name, SASL_MECHANISM_DIGEST_MD5);
911        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
912      }
913    }
914
915
916    // Make sure that the authID was provided.
917    if (authID == null || authID.length() == 0)
918    {
919      LocalizableMessage message =
920          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_DIGEST_MD5);
921      throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
922              message);
923    }
924
925
926    // Set password to ByteString.empty if the password is null.
927    if (bindPassword == null)
928    {
929        bindPassword = ByteString.empty();
930    }
931
932
933    sendInitialBindRequest(SASL_MECHANISM_DIGEST_MD5, bindDN);
934
935    LDAPMessage responseMessage1 =
936        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE, SASL_MECHANISM_DIGEST_MD5);
937    checkConnected(responseMessage1);
938
939    // Make sure that the bind response has the "SASL bind in progress" result code.
940    BindResponseProtocolOp bindResponse1 =
941         responseMessage1.getBindResponseProtocolOp();
942    int resultCode1 = bindResponse1.getResultCode();
943    if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get())
944    {
945      LocalizableMessage errorMessage = bindResponse1.getErrorMessage();
946      if (errorMessage == null)
947      {
948        errorMessage = LocalizableMessage.EMPTY;
949      }
950
951      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
952          get(SASL_MECHANISM_DIGEST_MD5, resultCode1,
953              ReturnCode.get(resultCode1), errorMessage);
954      throw new LDAPException(resultCode1, errorMessage, message,
955                              bindResponse1.getMatchedDN(), null);
956    }
957
958
959    // Make sure that the bind response contains SASL credentials with the
960    // information to use for the next stage of the bind.
961    ByteString serverCredentials =
962         bindResponse1.getServerSASLCredentials();
963    if (serverCredentials == null)
964    {
965      LocalizableMessage message = ERR_LDAPAUTH_NO_DIGESTMD5_SERVER_CREDENTIALS.get();
966      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
967    }
968
969
970    // Parse the server SASL credentials to get the necessary information.  In
971    // particular, look at the realm, the nonce, the QoP modes, and the charset.
972    // We'll only care about the realm if none was provided in the SASL
973    // properties and only one was provided in the server SASL credentials.
974    String  credString = serverCredentials.toString();
975    String  lowerCreds = toLowerCase(credString);
976    String  nonce      = null;
977    boolean useUTF8    = false;
978    int     pos        = 0;
979    int     length     = credString.length();
980    while (pos < length)
981    {
982      int equalPos = credString.indexOf('=', pos+1);
983      if (equalPos < 0)
984      {
985        // This is bad because we're not at the end of the string but we don't
986        // have a name/value delimiter.
987        LocalizableMessage message =
988            ERR_LDAPAUTH_DIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get(
989                    credString, pos);
990        throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
991      }
992
993
994      String tokenName  = lowerCreds.substring(pos, equalPos);
995
996      StringBuilder valueBuffer = new StringBuilder();
997      pos = readToken(credString, equalPos+1, length, valueBuffer);
998      String tokenValue = valueBuffer.toString();
999
1000      if (tokenName.equals("charset"))
1001      {
1002        // The value must be the string "utf-8".  If not, that's an error.
1003        if (! tokenValue.equalsIgnoreCase("utf-8"))
1004        {
1005          LocalizableMessage message =
1006              ERR_LDAPAUTH_DIGESTMD5_INVALID_CHARSET.get(tokenValue);
1007          throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1008        }
1009
1010        useUTF8 = true;
1011      }
1012      else if (tokenName.equals("realm"))
1013      {
1014        // This will only be of interest to us if there is only a single realm
1015        // in the server credentials and none was provided as a client-side
1016        // property.
1017        if (! realmSetFromProperty)
1018        {
1019          if (realm == null)
1020          {
1021            // No other realm was specified, so we'll use this one for now.
1022            realm = tokenValue;
1023          }
1024          else
1025          {
1026            // This must mean that there are multiple realms in the server
1027            // credentials.  In that case, we'll not provide any realm at all.
1028            // To make sure that happens, pretend that the client specified the
1029            // realm.
1030            realm                = null;
1031            realmSetFromProperty = true;
1032          }
1033        }
1034      }
1035      else if (tokenName.equals("nonce"))
1036      {
1037        nonce = tokenValue;
1038      }
1039      else if (tokenName.equals("qop"))
1040      {
1041        // The QoP modes provided by the server should be a comma-delimited
1042        // list.  Decode that list and make sure the QoP we have chosen is in
1043        // that list.
1044        StringTokenizer tokenizer = new StringTokenizer(tokenValue, ",");
1045        LinkedList<String> qopModes = new LinkedList<>();
1046        while (tokenizer.hasMoreTokens())
1047        {
1048          qopModes.add(toLowerCase(tokenizer.nextToken().trim()));
1049        }
1050
1051        if (! qopModes.contains(qop))
1052        {
1053          LocalizableMessage message = ERR_LDAPAUTH_REQUESTED_QOP_NOT_SUPPORTED_BY_SERVER.
1054              get(qop, tokenValue);
1055          throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1056                                    message);
1057        }
1058      }
1059      else
1060      {
1061        // Other values may have been provided, but they aren't of interest to
1062        // us because they shouldn't change anything about the way we encode the
1063        // second part of the request.  Rather than attempt to examine them,
1064        // we'll assume that the server sent a valid response.
1065      }
1066    }
1067
1068
1069    // Make sure that the nonce was included in the response from the server.
1070    if (nonce == null)
1071    {
1072      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_NONCE.get();
1073      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1074    }
1075
1076
1077    // Generate the cnonce that we will use for this request.
1078    String cnonce = generateCNonce();
1079
1080
1081    // Generate the response digest, and initialize the necessary remaining
1082    // variables to use in the generation of that digest.
1083    String nonceCount = "00000001";
1084    String charset    = useUTF8 ? "UTF-8" : "ISO-8859-1";
1085    String responseDigest;
1086    try
1087    {
1088      responseDigest = generateDigestMD5Response(authID, authzID,
1089                                                 bindPassword, realm,
1090                                                 nonce, cnonce, nonceCount,
1091                                                 digestURI, qop, charset);
1092    }
1093    catch (ClientException ce)
1094    {
1095      throw ce;
1096    }
1097    catch (Exception e)
1098    {
1099      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_CANNOT_CREATE_RESPONSE_DIGEST.
1100          get(getExceptionMessage(e));
1101      throw new ClientException(
1102              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1103    }
1104
1105
1106    // Generate the SASL credentials for the second bind request.
1107    StringBuilder credBuffer = new StringBuilder();
1108    credBuffer.append("username=\"").append(authID).append("\"");
1109    if (realm != null)
1110    {
1111      credBuffer.append(",realm=\"").append(realm).append("\"");
1112    }
1113    credBuffer.append(",nonce=\"").append(nonce);
1114    credBuffer.append("\",cnonce=\"").append(cnonce);
1115    credBuffer.append("\",nc=").append(nonceCount);
1116    credBuffer.append(",qop=").append(qop);
1117    credBuffer.append(",digest-uri=\"").append(digestURI);
1118    credBuffer.append("\",response=").append(responseDigest);
1119    if (useUTF8)
1120    {
1121      credBuffer.append(",charset=utf-8");
1122    }
1123    if (authzID != null)
1124    {
1125      credBuffer.append(",authzid=\"").append(authzID).append("\"");
1126    }
1127
1128    sendSecondBindRequest(SASL_MECHANISM_DIGEST_MD5, bindDN, credBuffer.toString(), requestControls);
1129
1130    LDAPMessage responseMessage2 =
1131        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE, SASL_MECHANISM_DIGEST_MD5);
1132    responseControls.addAll(responseMessage2.getControls());
1133    checkConnected(responseMessage2);
1134    BindResponseProtocolOp bindResponse2 = checkSuccessfulBind(responseMessage2, SASL_MECHANISM_DIGEST_MD5);
1135
1136
1137    // Make sure that the bind response included server SASL credentials with
1138    // the appropriate rspauth value.
1139    ByteString rspAuthCreds = bindResponse2.getServerSASLCredentials();
1140    if (rspAuthCreds == null)
1141    {
1142      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
1143      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1144    }
1145
1146    String credStr = toLowerCase(rspAuthCreds.toString());
1147    if (! credStr.startsWith("rspauth="))
1148    {
1149      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
1150      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1151    }
1152
1153
1154    byte[] serverRspAuth;
1155    try
1156    {
1157      serverRspAuth = hexStringToByteArray(credStr.substring(8));
1158    }
1159    catch (Exception e)
1160    {
1161      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_DECODE_RSPAUTH.get(
1162          getExceptionMessage(e));
1163      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1164    }
1165
1166    byte[] clientRspAuth;
1167    try
1168    {
1169      clientRspAuth =
1170           generateDigestMD5RspAuth(authID, authzID, bindPassword,
1171                                    realm, nonce, cnonce, nonceCount, digestURI,
1172                                    qop, charset);
1173    }
1174    catch (Exception e)
1175    {
1176      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_CALCULATE_RSPAUTH.get(
1177          getExceptionMessage(e));
1178      throw new ClientException(
1179              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1180    }
1181
1182    if (! Arrays.equals(serverRspAuth, clientRspAuth))
1183    {
1184      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_RSPAUTH_MISMATCH.get();
1185      throw new ClientException(
1186              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1187    }
1188
1189    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
1190    return null;
1191  }
1192
1193  private void sendSecondBindRequest(String saslMechanism, ByteSequence bindDN, String saslCredentials,
1194      List<Control> requestControls) throws ClientException
1195  {
1196    // Generate and send the second bind request.
1197    BindRequestProtocolOp bindRequest2 =
1198        new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, ByteString.valueOfUtf8(saslCredentials));
1199    LDAPMessage requestMessage2 = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2, requestControls);
1200
1201    try
1202    {
1203      writer.writeMessage(requestMessage2);
1204    }
1205    catch (IOException ioe)
1206    {
1207      LocalizableMessage message =
1208          ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe));
1209      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1210    }
1211    catch (Exception e)
1212    {
1213      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(saslMechanism, getExceptionMessage(e));
1214      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
1215    }
1216  }
1217
1218  /**
1219   * Reads the next token from the provided credentials string using the
1220   * provided information.  If the token is surrounded by quotation marks, then
1221   * the token returned will not include those quotation marks.
1222   *
1223   * @param  credentials  The credentials string from which to read the token.
1224   * @param  startPos     The position of the first character of the token to
1225   *                      read.
1226   * @param  length       The total number of characters in the credentials
1227   *                      string.
1228   * @param  token        The buffer into which the token is to be placed.
1229   *
1230   * @return  The position at which the next token should start, or a value
1231   *          greater than or equal to the length of the string if there are no
1232   *          more tokens.
1233   *
1234   * @throws  LDAPException  If a problem occurs while attempting to read the
1235   *                         token.
1236   */
1237  private int readToken(String credentials, int startPos, int length,
1238                        StringBuilder token)
1239          throws LDAPException
1240  {
1241    // If the position is greater than or equal to the length, then we shouldn't
1242    // do anything.
1243    if (startPos >= length)
1244    {
1245      return startPos;
1246    }
1247
1248
1249    // Look at the first character to see if it's an empty string or the string
1250    // is quoted.
1251    boolean isEscaped = false;
1252    boolean isQuoted  = false;
1253    int     pos       = startPos;
1254    char    c         = credentials.charAt(pos++);
1255
1256    if (c == ',')
1257    {
1258      // This must be a zero-length token, so we'll just return the next
1259      // position.
1260      return pos;
1261    }
1262    else if (c == '"')
1263    {
1264      // The string is quoted, so we'll ignore this character, and we'll keep
1265      // reading until we find the unescaped closing quote followed by a comma
1266      // or the end of the string.
1267      isQuoted = true;
1268    }
1269    else if (c == '\\')
1270    {
1271      // The next character is escaped, so we'll take it no matter what.
1272      isEscaped = true;
1273    }
1274    else
1275    {
1276      // The string is not quoted, and this is the first character.  Store this
1277      // character and keep reading until we find a comma or the end of the
1278      // string.
1279      token.append(c);
1280    }
1281
1282
1283    // Enter a loop, reading until we find the appropriate criteria for the end
1284    // of the token.
1285    while (pos < length)
1286    {
1287      c = credentials.charAt(pos++);
1288
1289      if (isEscaped)
1290      {
1291        // The previous character was an escape, so we'll take this no matter
1292        // what.
1293        token.append(c);
1294        isEscaped = false;
1295      }
1296      else if (c == ',')
1297      {
1298        // If this is a quoted string, then this comma is part of the token.
1299        // Otherwise, it's the end of the token.
1300        if (!isQuoted)
1301        {
1302          break;
1303        }
1304        token.append(c);
1305      }
1306      else if (c == '"')
1307      {
1308        if (isQuoted)
1309        {
1310          // This should be the end of the token, but in order for it to be
1311          // valid it must be followed by a comma or the end of the string.
1312          if (pos >= length)
1313          {
1314            // We have hit the end of the string, so this is fine.
1315            break;
1316          }
1317          char c2 = credentials.charAt(pos++);
1318          if (c2 == ',')
1319          {
1320            // We have hit the end of the token, so this is fine.
1321            break;
1322          }
1323          else
1324          {
1325            // We found the closing quote before the end of the token. This is not fine.
1326            LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_CLOSING_QUOTE_POS.get(pos - 2);
1327            throw new LDAPException(ReturnCode.INVALID_CREDENTIALS.get(), message);
1328          }
1329        }
1330        else
1331        {
1332          // This must be part of the value, so we'll take it.
1333          token.append(c);
1334        }
1335      }
1336      else if (c == '\\')
1337      {
1338        // The next character is escaped.  We'll set a flag so we know to
1339        // accept it, but will not include the backspace itself.
1340        isEscaped = true;
1341      }
1342      else
1343      {
1344        token.append(c);
1345      }
1346    }
1347
1348
1349    return pos;
1350  }
1351
1352
1353
1354  /**
1355   * Generates a cnonce value to use during the DIGEST-MD5 authentication
1356   * process.
1357   *
1358   * @return  The cnonce that should be used for DIGEST-MD5 authentication.
1359   */
1360  private String generateCNonce()
1361  {
1362    if (secureRandom == null)
1363    {
1364      secureRandom = new SecureRandom();
1365    }
1366
1367    byte[] cnonceBytes = new byte[16];
1368    secureRandom.nextBytes(cnonceBytes);
1369
1370    return Base64.encode(cnonceBytes);
1371  }
1372
1373
1374
1375  /**
1376   * Generates the appropriate DIGEST-MD5 response for the provided set of
1377   * information.
1378   *
1379   * @param  authID    The username from the authentication request.
1380   * @param  authzID     The authorization ID from the request, or
1381   *                     <CODE>null</CODE> if there is none.
1382   * @param  password    The clear-text password for the user.
1383   * @param  realm       The realm for which the authentication is to be
1384   *                     performed.
1385   * @param  nonce       The random data generated by the server for use in the
1386   *                     digest.
1387   * @param  cnonce      The random data generated by the client for use in the
1388   *                     digest.
1389   * @param  nonceCount  The 8-digit hex string indicating the number of times
1390   *                     the provided nonce has been used by the client.
1391   * @param  digestURI   The digest URI that specifies the service and host for
1392   *                     which the authentication is being performed.
1393   * @param  qop         The quality of protection string for the
1394   *                     authentication.
1395   * @param  charset     The character set used to encode the information.
1396   *
1397   * @return  The DIGEST-MD5 response for the provided set of information.
1398   *
1399   * @throws  ClientException  If a problem occurs while attempting to
1400   *                           initialize the MD5 digest.
1401   *
1402   * @throws  UnsupportedEncodingException  If the specified character set is
1403   *                                        invalid for some reason.
1404   */
1405  private String generateDigestMD5Response(String authID, String authzID,
1406                                           ByteSequence password, String realm,
1407                                           String nonce, String cnonce,
1408                                           String nonceCount, String digestURI,
1409                                           String qop, String charset)
1410          throws ClientException, UnsupportedEncodingException
1411  {
1412    // Perform the necessary initialization if it hasn't been done yet.
1413    if (md5Digest == null)
1414    {
1415      try
1416      {
1417        md5Digest = MessageDigest.getInstance("MD5");
1418      }
1419      catch (Exception e)
1420      {
1421        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
1422            getExceptionMessage(e));
1423        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
1424                message, e);
1425      }
1426    }
1427
1428    // Get a hash of "username:realm:password".
1429    String a1String1 = authID + ':' + ((realm == null) ? "" : realm) + ':';
1430    byte[] a1Bytes1a = a1String1.getBytes(charset);
1431    byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length()];
1432    System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
1433    password.copyTo(a1Bytes1, a1Bytes1a.length);
1434    byte[] urpHash = md5Digest.digest(a1Bytes1);
1435
1436    // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
1437    StringBuilder a1String2 = new StringBuilder();
1438    a1String2.append(':');
1439    a1String2.append(nonce);
1440    a1String2.append(':');
1441    a1String2.append(cnonce);
1442    if (authzID != null)
1443    {
1444      a1String2.append(':');
1445      a1String2.append(authzID);
1446    }
1447    byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
1448    byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
1449    System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
1450    System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length);
1451    byte[] a1Hash = md5Digest.digest(a1Bytes2);
1452
1453    // Next, get a hash of "AUTHENTICATE:digesturi".
1454    byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset);
1455    byte[] a2Hash  = md5Digest.digest(a2Bytes);
1456
1457    // Get hex string representations of the last two hashes.
1458    String a1HashHex = getHexString(a1Hash);
1459    String a2HashHex = getHexString(a2Hash);
1460
1461    // Put together the final string to hash, consisting of
1462    // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
1463    String kdStr = a1HashHex + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + a2HashHex;
1464    return getHexString(md5Digest.digest(kdStr.getBytes(charset)));
1465  }
1466
1467  /**
1468   * Generates the appropriate DIGEST-MD5 rspauth digest using the provided
1469   * information.
1470   *
1471   * @param  authID      The username from the authentication request.
1472   * @param  authzID     The authorization ID from the request, or
1473   *                     <CODE>null</CODE> if there is none.
1474   * @param  password    The clear-text password for the user.
1475   * @param  realm       The realm for which the authentication is to be
1476   *                     performed.
1477   * @param  nonce       The random data generated by the server for use in the
1478   *                     digest.
1479   * @param  cnonce      The random data generated by the client for use in the
1480   *                     digest.
1481   * @param  nonceCount  The 8-digit hex string indicating the number of times
1482   *                     the provided nonce has been used by the client.
1483   * @param  digestURI   The digest URI that specifies the service and host for
1484   *                     which the authentication is being performed.
1485   * @param  qop         The quality of protection string for the
1486   *                     authentication.
1487   * @param  charset     The character set used to encode the information.
1488   *
1489   * @return  The DIGEST-MD5 response for the provided set of information.
1490   *
1491   * @throws  UnsupportedEncodingException  If the specified character set is
1492   *                                        invalid for some reason.
1493   */
1494  private byte[] generateDigestMD5RspAuth(String authID, String authzID,
1495                                         ByteSequence password, String realm,
1496                                         String nonce, String cnonce,
1497                                         String nonceCount, String digestURI,
1498                                         String qop, String charset)
1499         throws UnsupportedEncodingException
1500  {
1501    // First, get a hash of "username:realm:password".
1502    String a1String1 = authID + ':' + realm + ':';
1503
1504    byte[] a1Bytes1a = a1String1.getBytes(charset);
1505    byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length()];
1506    System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
1507    password.copyTo(a1Bytes1, a1Bytes1a.length);
1508    byte[] urpHash = md5Digest.digest(a1Bytes1);
1509
1510
1511    // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
1512    StringBuilder a1String2 = new StringBuilder();
1513    a1String2.append(':');
1514    a1String2.append(nonce);
1515    a1String2.append(':');
1516    a1String2.append(cnonce);
1517    if (authzID != null)
1518    {
1519      a1String2.append(':');
1520      a1String2.append(authzID);
1521    }
1522    byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
1523    byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
1524    System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
1525    System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length,
1526                     a1Bytes2a.length);
1527    byte[] a1Hash = md5Digest.digest(a1Bytes2);
1528
1529
1530    // Next, get a hash of "AUTHENTICATE:digesturi".
1531    String a2String = ":" + digestURI;
1532    if (qop.equals("auth-int") || qop.equals("auth-conf"))
1533    {
1534      a2String += ":00000000000000000000000000000000";
1535    }
1536    byte[] a2Bytes = a2String.getBytes(charset);
1537    byte[] a2Hash  = md5Digest.digest(a2Bytes);
1538
1539
1540    // Get hex string representations of the last two hashes.
1541    String a1HashHex = getHexString(a1Hash);
1542    String a2HashHex = getHexString(a2Hash);
1543
1544    // Put together the final string to hash, consisting of
1545    // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
1546    String kdStr = a1HashHex + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + a2HashHex;
1547    return md5Digest.digest(kdStr.getBytes(charset));
1548  }
1549
1550  /**
1551   * Retrieves a hexadecimal string representation of the contents of the
1552   * provided byte array.
1553   *
1554   * @param  byteArray  The byte array for which to obtain the hexadecimal
1555   *                    string representation.
1556   *
1557   * @return  The hexadecimal string representation of the contents of the
1558   *          provided byte array.
1559   */
1560  private String getHexString(byte[] byteArray)
1561  {
1562    StringBuilder buffer = new StringBuilder(2*byteArray.length);
1563    for (byte b : byteArray)
1564    {
1565      buffer.append(byteToLowerHex(b));
1566    }
1567
1568    return buffer.toString();
1569  }
1570
1571
1572
1573  /**
1574   * Retrieves the set of properties that a client may provide when performing a
1575   * SASL DIGEST-MD5 bind, mapped from the property names to their corresponding
1576   * descriptions.
1577   *
1578   * @return  The set of properties that a client may provide when performing a
1579   *          SASL DIGEST-MD5 bind, mapped from the property names to their
1580   *          corresponding descriptions.
1581   */
1582  private static LinkedHashMap<String, LocalizableMessage> getSASLDigestMD5Properties()
1583  {
1584    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(5);
1585
1586    properties.put(SASL_PROPERTY_AUTHID,
1587                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
1588    properties.put(SASL_PROPERTY_REALM,
1589                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
1590    properties.put(SASL_PROPERTY_QOP,
1591                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_QOP.get());
1592    properties.put(SASL_PROPERTY_DIGEST_URI,
1593                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_DIGEST_URI.get());
1594    properties.put(SASL_PROPERTY_AUTHZID,
1595                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
1596
1597    return properties;
1598  }
1599
1600
1601
1602  /**
1603   * Processes a SASL EXTERNAL bind with the provided information.
1604   *
1605   * @param  bindDN            The DN to use to bind to the Directory Server, or
1606   *                           <CODE>null</CODE> if the authentication identity
1607   *                           is to be set through some other means.
1608   * @param  saslProperties    A set of additional properties that may be needed
1609   *                           to process the SASL bind.  SASL EXTERNAL does not
1610   *                           take any properties, so this should be empty or
1611   *                           <CODE>null</CODE>.
1612   * @param  requestControls   The set of controls to include the request to the
1613   *                           server.
1614   * @param  responseControls  A list to hold the set of controls included in
1615   *                           the response from the server.
1616   *
1617   * @return  A message providing additional information about the bind if
1618   *          appropriate, or <CODE>null</CODE> if there is no special
1619   *          information available.
1620   *
1621   * @throws  ClientException  If a client-side problem prevents the bind
1622   *                           attempt from succeeding.
1623   *
1624   * @throws  LDAPException  If the bind fails or some other server-side problem
1625   *                         occurs during processing.
1626   */
1627  public String doSASLExternal(ByteSequence bindDN,
1628                     Map<String,List<String>> saslProperties,
1629                     List<Control> requestControls,
1630                     List<Control> responseControls)
1631         throws ClientException, LDAPException
1632  {
1633    // Make sure that no SASL properties were provided.
1634    if (saslProperties != null && ! saslProperties.isEmpty())
1635    {
1636      LocalizableMessage message =
1637          ERR_LDAPAUTH_NO_ALLOWED_SASL_PROPERTIES.get(SASL_MECHANISM_EXTERNAL);
1638      throw new ClientException(
1639              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
1640    }
1641
1642
1643    sendBindRequest(SASL_MECHANISM_EXTERNAL, bindDN, null, requestControls);
1644
1645    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
1646    responseControls.addAll(responseMessage.getControls());
1647    checkConnected(responseMessage);
1648
1649    BindResponseProtocolOp bindResponse =
1650         responseMessage.getBindResponseProtocolOp();
1651    int resultCode = bindResponse.getResultCode();
1652    if (resultCode == ReturnCode.SUCCESS.get())
1653    {
1654      // FIXME -- Need to look for things like password expiration warning,
1655      // reset notice, etc.
1656      return null;
1657    }
1658
1659    // FIXME -- Add support for referrals.
1660
1661    LocalizableMessage message =
1662        ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_EXTERNAL);
1663    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
1664                            message, bindResponse.getMatchedDN(), null);
1665  }
1666
1667  private void sendBindRequest(String saslMechanism, ByteSequence bindDN, ByteString saslCredentials,
1668      List<Control> requestControls) throws ClientException
1669  {
1670    BindRequestProtocolOp bindRequest =
1671        new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, saslCredentials);
1672    LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, requestControls);
1673
1674    try
1675    {
1676      writer.writeMessage(requestMessage);
1677    }
1678    catch (IOException ioe)
1679    {
1680      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe));
1681      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1682    }
1683    catch (Exception e)
1684    {
1685      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(saslMechanism, getExceptionMessage(e));
1686      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
1687    }
1688  }
1689
1690  private LDAPMessage readBindResponse(Arg1<Object> errCannotReadBindResponse) throws ClientException
1691  {
1692    try
1693    {
1694      LDAPMessage responseMessage = reader.readMessage();
1695      if (responseMessage != null)
1696      {
1697        return responseMessage;
1698      }
1699      LocalizableMessage message = ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
1700      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message);
1701    }
1702    catch (DecodeException | LDAPException e)
1703    {
1704      LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(e));
1705      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
1706    }
1707    catch (IOException ioe)
1708    {
1709      LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(ioe));
1710      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1711    }
1712    catch (Exception e)
1713    {
1714      LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(e));
1715      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1716    }
1717  }
1718
1719  /**
1720   * Retrieves the set of properties that a client may provide when performing a
1721   * SASL EXTERNAL bind, mapped from the property names to their corresponding
1722   * descriptions.
1723   *
1724   * @return  The set of properties that a client may provide when performing a
1725   *          SASL EXTERNAL bind, mapped from the property names to their
1726   *          corresponding descriptions.
1727   */
1728  private static LinkedHashMap<String, LocalizableMessage> getSASLExternalProperties()
1729  {
1730    // There are no properties for the SASL EXTERNAL mechanism.
1731    return new LinkedHashMap<>(0);
1732  }
1733
1734
1735
1736  /**
1737   * Processes a SASL GSSAPI bind with the provided information.
1738   *
1739   * @param  bindDN            The DN to use to bind to the Directory Server, or
1740   *                           <CODE>null</CODE> if the authentication identity
1741   *                           is to be set through some other means.
1742   * @param  bindPassword      The password to use to bind to the Directory
1743   *                           Server.
1744   * @param  saslProperties    A set of additional properties that may be needed
1745   *                           to process the SASL bind.  SASL EXTERNAL does not
1746   *                           take any properties, so this should be empty or
1747   *                           <CODE>null</CODE>.
1748   * @param  requestControls   The set of controls to include the request to the
1749   *                           server.
1750   * @param  responseControls  A list to hold the set of controls included in
1751   *                           the response from the server.
1752   *
1753   * @return  A message providing additional information about the bind if
1754   *          appropriate, or <CODE>null</CODE> if there is no special
1755   *          information available.
1756   *
1757   * @throws  ClientException  If a client-side problem prevents the bind
1758   *                           attempt from succeeding.
1759   *
1760   * @throws  LDAPException  If the bind fails or some other server-side problem
1761   *                         occurs during processing.
1762   */
1763  private String doSASLGSSAPI(ByteSequence bindDN,
1764                     ByteSequence bindPassword,
1765                     Map<String,List<String>> saslProperties,
1766                     List<Control> requestControls,
1767                     List<Control> responseControls)
1768         throws ClientException, LDAPException
1769  {
1770    String kdc     = null;
1771    String realm   = null;
1772
1773    gssapiBindDN  = bindDN;
1774    gssapiAuthID  = null;
1775    gssapiAuthzID = null;
1776    gssapiQoP     = "auth";
1777    gssapiAuthPW = bindPassword != null ? bindPassword.toString().toCharArray() : null;
1778
1779    // Evaluate the properties provided.  The authID is required.  The authzID,
1780    // KDC, QoP, and realm are optional.
1781    if (saslProperties == null || saslProperties.isEmpty())
1782    {
1783      LocalizableMessage message =
1784          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_GSSAPI);
1785      throw new ClientException(
1786              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
1787    }
1788
1789    for (Entry<String, List<String>> entry : saslProperties.entrySet())
1790    {
1791      String name = entry.getKey();
1792      String lowerName = toLowerCase(name);
1793      List<String> values = entry.getValue();
1794
1795      if (lowerName.equals(SASL_PROPERTY_AUTHID))
1796      {
1797        gssapiAuthID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
1798      }
1799      else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
1800      {
1801        gssapiAuthzID = getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED);
1802      }
1803      else if (lowerName.equals(SASL_PROPERTY_KDC))
1804      {
1805        kdc = getSingleValue(values, ERR_LDAPAUTH_KDC_SINGLE_VALUED);
1806      }
1807      else if (lowerName.equals(SASL_PROPERTY_QOP))
1808      {
1809        Iterator<String> iterator = values.iterator();
1810        if (iterator.hasNext())
1811        {
1812          gssapiQoP = toLowerCase(iterator.next());
1813
1814          if (iterator.hasNext())
1815          {
1816            LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
1817            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1818                                      message);
1819          }
1820
1821          if (gssapiQoP.equals("auth"))
1822          {
1823            // This is always fine.
1824          }
1825          else if (gssapiQoP.equals("auth-int") ||
1826                   gssapiQoP.equals("auth-conf"))
1827          {
1828            // FIXME -- Add support for integrity and confidentiality.
1829            LocalizableMessage message =
1830                ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(gssapiQoP);
1831            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1832                                      message);
1833          }
1834          else
1835          {
1836            // This is an illegal value.
1837            LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_INVALID_QOP.get(gssapiQoP);
1838            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1839                                      message);
1840          }
1841        }
1842      }
1843      else if (lowerName.equals(SASL_PROPERTY_REALM))
1844      {
1845        realm = getSingleValue(values, ERR_LDAPAUTH_REALM_SINGLE_VALUED);
1846      }
1847      else
1848      {
1849        LocalizableMessage message =
1850            ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_GSSAPI);
1851        throw new ClientException(
1852                ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
1853      }
1854    }
1855
1856
1857    // Make sure that the authID was provided.
1858    if (gssapiAuthID == null || gssapiAuthID.length() == 0)
1859    {
1860      LocalizableMessage message =
1861          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_GSSAPI);
1862      throw new ClientException(
1863              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
1864    }
1865
1866
1867    // See if an authzID was provided.  If not, then use the authID.
1868    if (gssapiAuthzID == null)
1869    {
1870      gssapiAuthzID = gssapiAuthID;
1871    }
1872
1873
1874    // See if the realm and/or KDC were specified.  If so, then set properties
1875    // that will allow them to be used.  Otherwise, we'll hope that the
1876    // underlying system has a valid Kerberos client configuration.
1877    if (realm != null)
1878    {
1879      System.setProperty(KRBV_PROPERTY_REALM, realm);
1880    }
1881
1882    if (kdc != null)
1883    {
1884      System.setProperty(KRBV_PROPERTY_KDC, kdc);
1885    }
1886
1887
1888    // Since we're going to be using JAAS behind the scenes, we need to have a
1889    // JAAS configuration.  Rather than always requiring the user to provide it,
1890    // we'll write one to a temporary file that will be deleted when the JVM
1891    // exits.
1892    String configFileName;
1893    try
1894    {
1895      File tempFile = File.createTempFile("login", "conf");
1896      configFileName = tempFile.getAbsolutePath();
1897      tempFile.deleteOnExit();
1898      try (BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false))) {
1899        w.write(getClass().getName() + " {");
1900        w.newLine();
1901
1902        w.write("  com.sun.security.auth.module.Krb5LoginModule required " +
1903            "client=TRUE useTicketCache=TRUE;");
1904        w.newLine();
1905
1906        w.write("};");
1907        w.newLine();
1908      }
1909    }
1910    catch (Exception e)
1911    {
1912      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get(
1913          getExceptionMessage(e));
1914      throw new ClientException(
1915              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1916    }
1917
1918    System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName);
1919    System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "true");
1920
1921
1922    // The rest of this code must be executed via JAAS, so it will have to go
1923    // in the "run" method.
1924    LoginContext loginContext;
1925    try
1926    {
1927      loginContext = new LoginContext(getClass().getName(), this);
1928      loginContext.login();
1929    }
1930    catch (Exception e)
1931    {
1932      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED.get(
1933          getExceptionMessage(e));
1934      throw new ClientException(
1935              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1936    }
1937
1938    try
1939    {
1940      Subject.doAs(loginContext.getSubject(), this);
1941    }
1942    catch (Exception e)
1943    {
1944      if (e instanceof ClientException)
1945      {
1946        throw (ClientException) e;
1947      }
1948      else if (e instanceof LDAPException)
1949      {
1950        throw (LDAPException) e;
1951      }
1952
1953      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_REMOTE_AUTHENTICATION_FAILED.get(
1954              getExceptionMessage(e));
1955      throw new ClientException(
1956              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1957    }
1958
1959
1960    // FIXME -- Need to make sure we handle request and response controls properly,
1961    // and also check for any possible message to send back to the client.
1962    return null;
1963  }
1964
1965  private String getSingleValue(List<String> values, Arg0 singleValuedErrMsg) throws ClientException
1966  {
1967    Iterator<String> it = values.iterator();
1968    if (it.hasNext())
1969    {
1970      String result = it.next();
1971      if (it.hasNext())
1972      {
1973        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, singleValuedErrMsg.get());
1974      }
1975      return result;
1976    }
1977    return null;
1978  }
1979
1980  /**
1981   * Retrieves the set of properties that a client may provide when performing a
1982   * SASL EXTERNAL bind, mapped from the property names to their corresponding
1983   * descriptions.
1984   *
1985   * @return  The set of properties that a client may provide when performing a
1986   *          SASL EXTERNAL bind, mapped from the property names to their
1987   *          corresponding descriptions.
1988   */
1989  private static LinkedHashMap<String, LocalizableMessage> getSASLGSSAPIProperties()
1990  {
1991    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(4);
1992
1993    properties.put(SASL_PROPERTY_AUTHID,
1994                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
1995    properties.put(SASL_PROPERTY_AUTHZID,
1996                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
1997    properties.put(SASL_PROPERTY_KDC,
1998                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_KDC.get());
1999    properties.put(SASL_PROPERTY_REALM,
2000                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
2001
2002    return properties;
2003  }
2004
2005
2006
2007  /**
2008   * Processes a SASL PLAIN bind with the provided information.
2009   *
2010   * @param  bindDN            The DN to use to bind to the Directory Server, or
2011   *                           <CODE>null</CODE> if the authentication identity
2012   *                           is to be set through some other means.
2013   * @param  bindPassword      The password to use to bind to the Directory
2014   *                           Server.
2015   * @param  saslProperties    A set of additional properties that may be needed
2016   *                           to process the SASL bind.
2017   * @param  requestControls   The set of controls to include the request to the
2018   *                           server.
2019   * @param  responseControls  A list to hold the set of controls included in
2020   *                           the response from the server.
2021   *
2022   * @return  A message providing additional information about the bind if
2023   *          appropriate, or <CODE>null</CODE> if there is no special
2024   *          information available.
2025   *
2026   * @throws  ClientException  If a client-side problem prevents the bind
2027   *                           attempt from succeeding.
2028   *
2029   * @throws  LDAPException  If the bind fails or some other server-side problem
2030   *                         occurs during processing.
2031   */
2032  public String doSASLPlain(ByteSequence bindDN,
2033                     ByteSequence bindPassword,
2034                     Map<String,List<String>> saslProperties,
2035                     List<Control> requestControls,
2036                     List<Control> responseControls)
2037         throws ClientException, LDAPException
2038  {
2039    String authID  = null;
2040    String authzID = null;
2041
2042
2043    // Evaluate the properties provided.  The authID is required, and authzID is
2044    // optional.
2045    if (saslProperties == null || saslProperties.isEmpty())
2046    {
2047      LocalizableMessage message =
2048          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_PLAIN);
2049      throw new ClientException(
2050              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2051    }
2052
2053    for (Entry<String, List<String>> entry : saslProperties.entrySet())
2054    {
2055      String name = entry.getKey();
2056      List<String> values = entry.getValue();
2057      String lowerName = toLowerCase(name);
2058
2059      if (lowerName.equals(SASL_PROPERTY_AUTHID))
2060      {
2061        authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
2062      }
2063      else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
2064      {
2065        authzID = getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED);
2066      }
2067      else
2068      {
2069        LocalizableMessage message =
2070            ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_PLAIN);
2071        throw new ClientException(
2072                ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2073      }
2074    }
2075
2076
2077    // Make sure that at least the authID was provided.
2078    if (authID == null || authID.length() == 0)
2079    {
2080      LocalizableMessage message =
2081          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_PLAIN);
2082      throw new ClientException(
2083              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2084    }
2085
2086
2087    // Set password to ByteString.empty if the password is null.
2088    if (bindPassword == null)
2089    {
2090        bindPassword = ByteString.empty();
2091    }
2092
2093    // Construct the bind request and send it to the server.
2094    String saslCredentials = (authzID != null ? authzID : "") + '\u0000' + authID + '\u0000' + bindPassword;
2095    sendBindRequest(SASL_MECHANISM_PLAIN, bindDN, ByteString.valueOfUtf8(saslCredentials), requestControls);
2096
2097    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
2098    responseControls.addAll(responseMessage.getControls());
2099    checkConnected(responseMessage);
2100    checkSuccessfulBind(responseMessage, SASL_MECHANISM_PLAIN);
2101    return null;
2102  }
2103
2104  /**
2105   * Retrieves the set of properties that a client may provide when performing a
2106   * SASL PLAIN bind, mapped from the property names to their corresponding
2107   * descriptions.
2108   *
2109   * @return  The set of properties that a client may provide when performing a
2110   *          SASL PLAIN bind, mapped from the property names to their
2111   *          corresponding descriptions.
2112   */
2113  private static LinkedHashMap<String, LocalizableMessage> getSASLPlainProperties()
2114  {
2115    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(2);
2116
2117    properties.put(SASL_PROPERTY_AUTHID,
2118                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
2119    properties.put(SASL_PROPERTY_AUTHZID,
2120                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
2121
2122    return properties;
2123  }
2124
2125
2126
2127  /**
2128   * Performs a privileged operation under JAAS so that the local authentication
2129   * information can be available for the SASL bind to the Directory Server.
2130   *
2131   * @return  A placeholder object in order to comply with the
2132   *          <CODE>PrivilegedExceptionAction</CODE> interface.
2133   *
2134   * @throws  ClientException  If a client-side problem occurs during the bind
2135   *                           processing.
2136   *
2137   * @throws  LDAPException  If a server-side problem occurs during the bind
2138   *                         processing.
2139   */
2140  @Override
2141  public Object run() throws ClientException, LDAPException
2142  {
2143    if (saslMechanism == null)
2144    {
2145      LocalizableMessage message = ERR_LDAPAUTH_NONSASL_RUN_INVOCATION.get(getBacktrace());
2146      throw new ClientException(
2147              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2148    }
2149    else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
2150    {
2151      doSASLGSSAPI2();
2152      return null;
2153    }
2154    else
2155    {
2156      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RUN_INVOCATION.get(
2157          saslMechanism, getBacktrace());
2158      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2159    }
2160  }
2161
2162  private void doSASLGSSAPI2() throws ClientException, LDAPException
2163  {
2164    // Create the property map that will be used by the internal SASL handler.
2165    Map<String, String> saslProperties = new HashMap<>();
2166    saslProperties.put(Sasl.QOP, gssapiQoP);
2167    saslProperties.put(Sasl.SERVER_AUTH, "true");
2168
2169
2170    // Create the SASL client that we will use to actually perform the
2171    // authentication.
2172    SaslClient saslClient;
2173    try
2174    {
2175      saslClient =
2176           Sasl.createSaslClient(new String[] { SASL_MECHANISM_GSSAPI },
2177                                 gssapiAuthzID, "ldap", hostName,
2178                                 saslProperties, this);
2179    }
2180    catch (Exception e)
2181    {
2182      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get(
2183          getExceptionMessage(e));
2184      throw new ClientException(
2185              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2186    }
2187
2188    // FIXME -- Add controls here?
2189    ByteString saslCredentials = getSaslCredentialsForInitialBind(saslClient);
2190    sendBindRequest(SASL_MECHANISM_GSSAPI, gssapiBindDN, saslCredentials, null);
2191
2192    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
2193    // FIXME -- Handle response controls.
2194    checkConnected(responseMessage);
2195
2196    while (true)
2197    {
2198      BindResponseProtocolOp bindResponse =
2199           responseMessage.getBindResponseProtocolOp();
2200      int resultCode = bindResponse.getResultCode();
2201      if (resultCode == ReturnCode.SUCCESS.get())
2202      {
2203        evaluateGSSAPIChallenge(saslClient, bindResponse);
2204        break;
2205      }
2206      else if (resultCode == ReturnCode.SASL_BIND_IN_PROGRESS.get())
2207      {
2208        // FIXME -- Add controls here?
2209        ByteString credBytes = evaluateSaslChallenge(saslClient, bindResponse);
2210        sendBindRequest(SASL_MECHANISM_GSSAPI, gssapiBindDN, credBytes, null);
2211
2212        responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
2213        // FIXME -- Handle response controls.
2214        checkConnected(responseMessage);
2215      }
2216      else
2217      {
2218        // This is an error.
2219        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get();
2220        throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
2221                                message, bindResponse.getMatchedDN(),
2222                                null);
2223      }
2224    }
2225    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
2226  }
2227
2228
2229  private void evaluateGSSAPIChallenge(SaslClient saslClient, BindResponseProtocolOp bindResponse)
2230      throws ClientException
2231  {
2232    // We should be done after this, but we still need to look for and
2233    // handle the server SASL credentials.
2234    ByteString serverSASLCredentials = bindResponse.getServerSASLCredentials();
2235    if (serverSASLCredentials != null)
2236    {
2237      try
2238      {
2239        saslClient.evaluateChallenge(serverSASLCredentials.toByteArray());
2240      }
2241      catch (Exception e)
2242      {
2243        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.get(getExceptionMessage(e));
2244        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2245      }
2246    }
2247
2248    // Just to be sure, check that the login really is complete.
2249    if (!saslClient.isComplete())
2250    {
2251      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_UNEXPECTED_SUCCESS_RESPONSE.get();
2252      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2253    }
2254  }
2255
2256  private ByteString evaluateSaslChallenge(SaslClient saslClient, BindResponseProtocolOp bindResponse)
2257      throws ClientException
2258  {
2259    try
2260    {
2261      ByteString saslCredentials = bindResponse.getServerSASLCredentials();
2262      byte[] bs = saslCredentials != null ? saslCredentials.toByteArray() : new byte[0];
2263      return ByteString.wrap(saslClient.evaluateChallenge(bs));
2264    }
2265    catch (Exception e)
2266    {
2267      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.get(getExceptionMessage(e));
2268      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2269    }
2270  }
2271
2272  private ByteString getSaslCredentialsForInitialBind(SaslClient saslClient) throws ClientException
2273  {
2274    if (saslClient.hasInitialResponse())
2275    {
2276      try
2277      {
2278        return ByteString.wrap(saslClient.evaluateChallenge(new byte[0]));
2279      }
2280      catch (Exception e)
2281      {
2282        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_INITIAL_CHALLENGE.get(getExceptionMessage(e));
2283        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2284      }
2285    }
2286    return null;
2287  }
2288
2289  /**
2290   * Look at the protocol op from the response.
2291   * If it's a bind response, then continue.
2292   * If it's an extended response, then check it is not a notice of disconnection.
2293   * Otherwise, generate an error.
2294   */
2295  private void checkConnected(LDAPMessage responseMessage) throws LDAPException, ClientException
2296  {
2297    switch (responseMessage.getProtocolOpType())
2298    {
2299      case OP_TYPE_BIND_RESPONSE:
2300        // We'll deal with this later.
2301        break;
2302
2303      case OP_TYPE_EXTENDED_RESPONSE:
2304        ExtendedResponseProtocolOp extendedResponse =
2305             responseMessage.getExtendedResponseProtocolOp();
2306        String responseOID = extendedResponse.getOID();
2307        if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID))
2308        {
2309          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
2310              get(extendedResponse.getResultCode(), extendedResponse.getErrorMessage());
2311          throw new LDAPException(extendedResponse.getResultCode(), message);
2312        }
2313        else
2314        {
2315          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
2316          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2317        }
2318
2319      default:
2320        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp());
2321        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2322    }
2323  }
2324
2325  /**
2326   * Handles the authentication callbacks to provide information needed by the
2327   * JAAS login process.
2328   *
2329   * @param  callbacks  The callbacks needed to provide information for the JAAS
2330   *                    login process.
2331   *
2332   * @throws  UnsupportedCallbackException  If an unexpected callback is
2333   *                                        included in the provided set.
2334   */
2335  @Override
2336  public void handle(Callback[] callbacks)
2337         throws UnsupportedCallbackException
2338  {
2339    if (saslMechanism ==  null)
2340    {
2341      LocalizableMessage message =
2342          ERR_LDAPAUTH_NONSASL_CALLBACK_INVOCATION.get(getBacktrace());
2343      throw new UnsupportedCallbackException(callbacks[0], message.toString());
2344    }
2345    else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
2346    {
2347      for (Callback cb : callbacks)
2348      {
2349        if (cb instanceof NameCallback)
2350        {
2351          ((NameCallback) cb).setName(gssapiAuthID);
2352        }
2353        else if (cb instanceof PasswordCallback)
2354        {
2355          if (gssapiAuthPW == null)
2356          {
2357            System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(gssapiAuthID));
2358            try
2359            {
2360              gssapiAuthPW = ConsoleApplication.readPassword();
2361            }
2362            catch (ClientException e)
2363            {
2364              throw new UnsupportedCallbackException(cb, e.getLocalizedMessage());
2365            }
2366          }
2367
2368          ((PasswordCallback) cb).setPassword(gssapiAuthPW);
2369        }
2370        else
2371        {
2372          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_GSSAPI_CALLBACK.get(cb);
2373          throw new UnsupportedCallbackException(cb, message.toString());
2374        }
2375      }
2376    }
2377    else
2378    {
2379      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_CALLBACK_INVOCATION.get(
2380          saslMechanism, getBacktrace());
2381      throw new UnsupportedCallbackException(callbacks[0], message.toString());
2382    }
2383  }
2384
2385
2386
2387  /**
2388   * Uses the "Who Am I?" extended operation to request that the server provide
2389   * the client with the authorization identity for this connection.
2390   *
2391   * @return  An ASN.1 octet string containing the authorization identity, or
2392   *          <CODE>null</CODE> if the client is not authenticated or is
2393   *          authenticated anonymously.
2394   *
2395   * @throws  ClientException  If a client-side problem occurs during the
2396   *                           request processing.
2397   *
2398   * @throws  LDAPException  If a server-side problem occurs during the request
2399   *                         processing.
2400   */
2401  public ByteString requestAuthorizationIdentity()
2402         throws ClientException, LDAPException
2403  {
2404    // Construct the extended request and send it to the server.
2405    ExtendedRequestProtocolOp extendedRequest =
2406         new ExtendedRequestProtocolOp(OID_WHO_AM_I_REQUEST);
2407    LDAPMessage requestMessage =
2408         new LDAPMessage(nextMessageID.getAndIncrement(), extendedRequest);
2409
2410    try
2411    {
2412      writer.writeMessage(requestMessage);
2413    }
2414    catch (IOException ioe)
2415    {
2416      LocalizableMessage message =
2417          ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(ioe));
2418      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
2419              message, ioe);
2420    }
2421    catch (Exception e)
2422    {
2423      LocalizableMessage message =
2424          ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(e));
2425      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
2426                                message, e);
2427    }
2428
2429
2430    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE);
2431
2432    // If the protocol op isn't an extended response, then that's a problem.
2433    if (responseMessage.getProtocolOpType() != OP_TYPE_EXTENDED_RESPONSE)
2434    {
2435      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp());
2436      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2437    }
2438
2439
2440    // Get the extended response and see if it has the "notice of disconnection"
2441    // OID.  If so, then the server is closing the connection.
2442    ExtendedResponseProtocolOp extendedResponse =
2443         responseMessage.getExtendedResponseProtocolOp();
2444    String responseOID = extendedResponse.getOID();
2445    if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID))
2446    {
2447      LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.get(
2448          extendedResponse.getResultCode(), extendedResponse.getErrorMessage());
2449      throw new LDAPException(extendedResponse.getResultCode(), message);
2450    }
2451
2452
2453    // It isn't a notice of disconnection so it must be the "Who Am I?"
2454    // response and the value would be the authorization ID.  However, first
2455    // check that it was successful.  If it was not, then fail.
2456    int resultCode = extendedResponse.getResultCode();
2457    if (resultCode != ReturnCode.SUCCESS.get())
2458    {
2459      LocalizableMessage message = ERR_LDAPAUTH_WHOAMI_FAILED.get();
2460      throw new LDAPException(resultCode, extendedResponse.getErrorMessage(),
2461                              message, extendedResponse.getMatchedDN(),
2462                              null);
2463    }
2464
2465
2466    // Get the authorization ID (if there is one) and return it to the caller.
2467    ByteString authzID = extendedResponse.getValue();
2468    if (authzID == null || authzID.length() == 0)
2469    {
2470      return null;
2471    }
2472
2473    if (!"dn:".equalsIgnoreCase(authzID.toString()))
2474    {
2475      return authzID;
2476    }
2477    return null;
2478  }
2479}