001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.tools;
028    import org.opends.messages.Message;
029    
030    
031    
032    import java.io.BufferedWriter;
033    import java.io.File;
034    import java.io.FileWriter;
035    import java.io.IOException;
036    import java.io.UnsupportedEncodingException;
037    import java.security.MessageDigest;
038    import java.security.PrivilegedExceptionAction;
039    import java.security.SecureRandom;
040    import java.util.ArrayList;
041    import java.util.Arrays;
042    import java.util.HashMap;
043    import java.util.Iterator;
044    import java.util.LinkedHashMap;
045    import java.util.LinkedList;
046    import java.util.List;
047    import java.util.Map;
048    import java.util.StringTokenizer;
049    import java.util.concurrent.atomic.AtomicInteger;
050    import javax.security.auth.Subject;
051    import javax.security.auth.callback.Callback;
052    import javax.security.auth.callback.CallbackHandler;
053    import javax.security.auth.callback.NameCallback;
054    import javax.security.auth.callback.PasswordCallback;
055    import javax.security.auth.callback.UnsupportedCallbackException;
056    import javax.security.auth.login.LoginContext;
057    import javax.security.sasl.Sasl;
058    import javax.security.sasl.SaslClient;
059    
060    import org.opends.server.protocols.asn1.ASN1Exception;
061    import org.opends.server.protocols.asn1.ASN1OctetString;
062    import org.opends.server.protocols.ldap.BindRequestProtocolOp;
063    import org.opends.server.protocols.ldap.BindResponseProtocolOp;
064    import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp;
065    import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp;
066    import org.opends.server.protocols.ldap.LDAPControl;
067    import org.opends.server.protocols.ldap.LDAPMessage;
068    import org.opends.server.protocols.ldap.LDAPResultCode;
069    import org.opends.server.types.LDAPException;
070    import org.opends.server.util.Base64;
071    import org.opends.server.util.PasswordReader;
072    
073    import static org.opends.messages.ToolMessages.*;
074    
075    import static org.opends.server.protocols.ldap.LDAPConstants.*;
076    import static org.opends.server.tools.ToolConstants.*;
077    import static org.opends.server.util.ServerConstants.*;
078    import static org.opends.server.util.StaticUtils.*;
079    
080    
081    
082    /**
083     * This class provides a generic interface that LDAP clients can use to perform
084     * various kinds of authentication to the Directory Server.  This handles both
085     * simple authentication as well as several SASL mechanisms including:
086     * <UL>
087     *   <LI>ANONYMOUS</LI>
088     *   <LI>CRAM-MD5</LI>
089     *   <LI>DIGEST-MD5</LI>
090     *   <LI>EXTERNAL</LI>
091     *   <LI>GSSAPI</LI>
092     *   <LI>PLAIN</LI>
093     * </UL>
094     * <BR><BR>
095     * Note that this implementation is not threadsafe, so if the same
096     * <CODE>AuthenticationHandler</CODE> object is to be used concurrently by
097     * multiple threads, it must be externally synchronized.
098     */
099    public class LDAPAuthenticationHandler
100           implements PrivilegedExceptionAction<Object>, CallbackHandler
101    {
102      // The bind DN for GSSAPI authentication.
103      private ASN1OctetString gssapiBindDN;
104    
105      // The LDAP reader that will be used to read data from the server.
106      private LDAPReader reader;
107    
108      // The LDAP writer that will be used to send data to the server.
109      private LDAPWriter writer;
110    
111      // The atomic integer that will be used to obtain message IDs for request
112      // messages.
113      private AtomicInteger nextMessageID;
114    
115      // An array filled with the inner pad byte.
116      private byte[] iPad;
117    
118      // An array filled with the outer pad byte.
119      private byte[] oPad;
120    
121      // The authentication password for GSSAPI authentication.
122      private char[] gssapiAuthPW;
123    
124      // The message digest that will be used to create MD5 hashes.
125      private MessageDigest md5Digest;
126    
127      // The secure random number generator for use by this authentication handler.
128      private SecureRandom secureRandom;
129    
130      // The authentication ID for GSSAPI authentication.
131      private String gssapiAuthID;
132    
133      // The authorization ID for GSSAPI authentication.
134      private String gssapiAuthzID;
135    
136      // The quality of protection for GSSAPI authentication.
137      private String gssapiQoP;
138    
139      // The host name used to connect to the remote system.
140      private String hostName;
141    
142      // The SASL mechanism that will be used for callback authentication.
143      private String saslMechanism;
144    
145    
146    
147      /**
148       * Creates a new instance of this authentication handler.  All initialization
149       * will be done lazily to avoid unnecessary performance hits, particularly
150       * for cases in which simple authentication will be used as it does not
151       * require any particularly expensive processing.
152       *
153       * @param  reader         The LDAP reader that will be used to read data from
154       *                        the server.
155       * @param  writer         The LDAP writer that will be used to send data to
156       *                        the server.
157       * @param  hostName       The host name used to connect to the remote system
158       *                        (fully-qualified if possible).
159       * @param  nextMessageID  The atomic integer that will be used to obtain
160       *                        message IDs for request messages.
161       */
162      public LDAPAuthenticationHandler(LDAPReader reader, LDAPWriter writer,
163                                       String hostName, AtomicInteger nextMessageID)
164      {
165        this.reader = reader;
166        this.writer = writer;
167        this.hostName      = hostName;
168        this.nextMessageID = nextMessageID;
169    
170        md5Digest    = null;
171        secureRandom = null;
172        iPad         = null;
173        oPad         = null;
174      }
175    
176    
177    
178      /**
179       * Retrieves a list of the SASL mechanisms that are supported by this client
180       * library.
181       *
182       * @return  A list of the SASL mechanisms that are supported by this client
183       *          library.
184       */
185      public static String[] getSupportedSASLMechanisms()
186      {
187        return new String[]
188        {
189          SASL_MECHANISM_ANONYMOUS,
190          SASL_MECHANISM_CRAM_MD5,
191          SASL_MECHANISM_DIGEST_MD5,
192          SASL_MECHANISM_EXTERNAL,
193          SASL_MECHANISM_GSSAPI,
194          SASL_MECHANISM_PLAIN
195        };
196      }
197    
198    
199    
200      /**
201       * Retrieves a list of the SASL properties that may be provided for the
202       * specified SASL mechanism, mapped from the property names to their
203       * corresponding descriptions.
204       *
205       * @param  mechanism  The name of the SASL mechanism for which to obtain the
206       *                    list of supported properties.
207       *
208       * @return  A list of the SASL properties that may be provided for the
209       *          specified SASL mechanism, mapped from the property names to their
210       *          corresponding descriptions.
211       */
212      public static LinkedHashMap<String,Message> getSASLProperties(
213              String mechanism)
214      {
215        String upperName = toUpperCase(mechanism);
216        if (upperName.equals(SASL_MECHANISM_ANONYMOUS))
217        {
218          return getSASLAnonymousProperties();
219        }
220        else if (upperName.equals(SASL_MECHANISM_CRAM_MD5))
221        {
222          return getSASLCRAMMD5Properties();
223        }
224        else if (upperName.equals(SASL_MECHANISM_DIGEST_MD5))
225        {
226          return getSASLDigestMD5Properties();
227        }
228        else if (upperName.equals(SASL_MECHANISM_EXTERNAL))
229        {
230          return getSASLExternalProperties();
231        }
232        else if (upperName.equals(SASL_MECHANISM_GSSAPI))
233        {
234          return getSASLGSSAPIProperties();
235        }
236        else if (upperName.equals(SASL_MECHANISM_PLAIN))
237        {
238          return getSASLPlainProperties();
239        }
240        else
241        {
242          // This is an unsupported mechanism.
243          return null;
244        }
245      }
246    
247    
248    
249      /**
250       * Processes a bind using simple authentication with the provided information.
251       * If the bind fails, then an exception will be thrown with information about
252       * the reason for the failure.  If the bind is successful but there may be
253       * some special information that the client should be given, then it will be
254       * returned as a String.
255       *
256       * @param  ldapVersion       The LDAP protocol version to use for the bind
257       *                           request.
258       * @param  bindDN            The DN to use to bind to the Directory Server, or
259       *                           <CODE>null</CODE> if it is to be an anonymous
260       *                           bind.
261       * @param  bindPassword      The password to use to bind to the Directory
262       *                           Server, or <CODE>null</CODE> if it is to be an
263       *                           anonymous bind.
264       * @param  requestControls   The set of controls to include the request to the
265       *                           server.
266       * @param  responseControls  A list to hold the set of controls included in
267       *                           the response from the server.
268       *
269       * @return  A message providing additional information about the bind if
270       *          appropriate, or <CODE>null</CODE> if there is no special
271       *          information available.
272       *
273       * @throws  ClientException  If a client-side problem prevents the bind
274       *                           attempt from succeeding.
275       *
276       * @throws  LDAPException  If the bind fails or some other server-side problem
277       *                         occurs during processing.
278       */
279      public String doSimpleBind(int ldapVersion, ASN1OctetString bindDN,
280                                 ASN1OctetString bindPassword,
281                                 ArrayList<LDAPControl> requestControls,
282                                 ArrayList<LDAPControl> responseControls)
283             throws ClientException, LDAPException
284      {
285        // See if we need to prompt the user for the password.
286        if (bindPassword == null)
287        {
288          if (bindDN == null)
289          {
290            bindPassword = new ASN1OctetString();
291          }
292          else
293          {
294            System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(
295                    bindDN.stringValue()));
296            System.out.flush();
297            char[] pwChars = PasswordReader.readPassword();
298            if (pwChars == null)
299            {
300              bindPassword = new ASN1OctetString();
301            }
302            else
303            {
304              bindPassword = new ASN1OctetString(getBytes(pwChars));
305              Arrays.fill(pwChars, '\u0000');
306            }
307          }
308        }
309    
310    
311        // Make sure that critical elements aren't null.
312        if (bindDN == null)
313        {
314          bindDN = new ASN1OctetString();
315        }
316    
317    
318        // Create the bind request and send it to the server.
319        BindRequestProtocolOp bindRequest =
320             new BindRequestProtocolOp(bindDN, ldapVersion, bindPassword);
321        LDAPMessage bindRequestMessage =
322             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
323                             requestControls);
324    
325        try
326        {
327          writer.writeMessage(bindRequestMessage);
328        }
329        catch (IOException ioe)
330        {
331          Message message =
332              ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(ioe));
333          throw new ClientException(
334                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
335        }
336        catch (Exception e)
337        {
338          Message message =
339              ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(e));
340          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
341                                    message, e);
342        }
343    
344    
345        // Read the response from the server.
346        LDAPMessage responseMessage;
347        try
348        {
349          responseMessage = reader.readMessage();
350          if (responseMessage == null)
351          {
352            Message message =
353                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
354            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
355                                      message);
356          }
357        }
358        catch (IOException ioe)
359        {
360          Message message =
361              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
362          throw new ClientException(
363                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
364        }
365        catch (ASN1Exception ae)
366        {
367          Message message =
368              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
369          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
370                                    message, ae);
371        }
372        catch (LDAPException le)
373        {
374          Message message =
375              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
376          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
377                                    message, le);
378        }
379        catch (Exception e)
380        {
381          Message message =
382              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
383          throw new ClientException(
384                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
385        }
386    
387    
388        // See if there are any controls in the response.  If so, then add them to
389        // the response controls list.
390        ArrayList<LDAPControl> respControls = responseMessage.getControls();
391        if ((respControls != null) && (! respControls.isEmpty()))
392        {
393          responseControls.addAll(respControls);
394        }
395    
396    
397        // Look at the protocol op from the response.  If it's a bind response, then
398        // continue.  If it's an extended response, then it could be a notice of
399        // disconnection so check for that.  Otherwise, generate an error.
400        switch (responseMessage.getProtocolOpType())
401        {
402          case OP_TYPE_BIND_RESPONSE:
403            // We'll deal with this later.
404            break;
405    
406          case OP_TYPE_EXTENDED_RESPONSE:
407            ExtendedResponseProtocolOp extendedResponse =
408                 responseMessage.getExtendedResponseProtocolOp();
409            String responseOID = extendedResponse.getOID();
410            if ((responseOID != null) &&
411                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
412            {
413              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
414                  get(extendedResponse.getResultCode(),
415                      extendedResponse.getErrorMessage());
416              throw new LDAPException(extendedResponse.getResultCode(), message);
417            }
418            else
419            {
420              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
421                  String.valueOf(extendedResponse));
422              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
423                                        message);
424            }
425    
426          default:
427            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
428                String.valueOf(responseMessage.getProtocolOp()));
429            throw new ClientException(
430                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
431        }
432    
433    
434        BindResponseProtocolOp bindResponse =
435             responseMessage.getBindResponseProtocolOp();
436        int resultCode = bindResponse.getResultCode();
437        if (resultCode == LDAPResultCode.SUCCESS)
438        {
439          // FIXME -- Need to look for things like password expiration warning,
440          // reset notice, etc.
441          return null;
442        }
443    
444        // FIXME -- Add support for referrals.
445    
446        Message message = ERR_LDAPAUTH_SIMPLE_BIND_FAILED.get();
447        throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
448                                message, bindResponse.getMatchedDN(), null);
449      }
450    
451    
452    
453      /**
454       * Processes a SASL bind using the provided information.  If the bind fails,
455       * then an exception will be thrown with information about the reason for the
456       * failure.  If the bind is successful but there may be some special
457       * information that the client should be given, then it will be returned as a
458       * String.
459       *
460       * @param  bindDN            The DN to use to bind to the Directory Server, or
461       *                           <CODE>null</CODE> if the authentication identity
462       *                           is to be set through some other means.
463       * @param  bindPassword      The password to use to bind to the Directory
464       *                           Server, or <CODE>null</CODE> if this is not a
465       *                           password-based SASL mechanism.
466       * @param  mechanism         The name of the SASL mechanism to use to
467       *                           authenticate to the Directory Server.
468       * @param  saslProperties    A set of additional properties that may be needed
469       *                           to process the SASL bind.
470       * @param  requestControls   The set of controls to include the request to the
471       *                           server.
472       * @param  responseControls  A list to hold the set of controls included in
473       *                           the response from the server.
474       *
475       * @return  A message providing additional information about the bind if
476       *          appropriate, or <CODE>null</CODE> if there is no special
477       *          information available.
478       *
479       * @throws  ClientException  If a client-side problem prevents the bind
480       *                           attempt from succeeding.
481       *
482       * @throws  LDAPException  If the bind fails or some other server-side problem
483       *                         occurs during processing.
484       */
485      public String doSASLBind(ASN1OctetString bindDN, ASN1OctetString bindPassword,
486                               String mechanism,
487                               Map<String,List<String>> saslProperties,
488                               ArrayList<LDAPControl> requestControls,
489                               ArrayList<LDAPControl> responseControls)
490             throws ClientException, LDAPException
491      {
492        // Make sure that critical elements aren't null.
493        if (bindDN == null)
494        {
495          bindDN = new ASN1OctetString();
496        }
497    
498        if ((mechanism == null) || (mechanism.length() == 0))
499        {
500          Message message = ERR_LDAPAUTH_NO_SASL_MECHANISM.get();
501          throw new ClientException(
502                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
503        }
504    
505    
506        // Look at the mechanism name and call the appropriate method to process
507        // the request.
508        saslMechanism = toUpperCase(mechanism);
509        if (saslMechanism.equals(SASL_MECHANISM_ANONYMOUS))
510        {
511          return doSASLAnonymous(bindDN, saslProperties, requestControls,
512                                 responseControls);
513        }
514        else if (saslMechanism.equals(SASL_MECHANISM_CRAM_MD5))
515        {
516          return doSASLCRAMMD5(bindDN, bindPassword, saslProperties,
517                               requestControls, responseControls);
518        }
519        else if (saslMechanism.equals(SASL_MECHANISM_DIGEST_MD5))
520        {
521          return doSASLDigestMD5(bindDN, bindPassword, saslProperties,
522                                 requestControls, responseControls);
523        }
524        else if (saslMechanism.equals(SASL_MECHANISM_EXTERNAL))
525        {
526          return doSASLExternal(bindDN, saslProperties, requestControls,
527                                responseControls);
528        }
529        else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
530        {
531          return doSASLGSSAPI(bindDN, bindPassword, saslProperties, requestControls,
532                              responseControls);
533        }
534        else if (saslMechanism.equals(SASL_MECHANISM_PLAIN))
535        {
536          return doSASLPlain(bindDN, bindPassword, saslProperties, requestControls,
537                             responseControls);
538        }
539        else
540        {
541          Message message = ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mechanism);
542          throw new ClientException(
543                  LDAPResultCode.CLIENT_SIDE_AUTH_UNKNOWN, message);
544        }
545      }
546    
547    
548    
549      /**
550       * Processes a SASL ANONYMOUS bind with the provided information.
551       *
552       * @param  bindDN            The DN to use to bind to the Directory Server, or
553       *                           <CODE>null</CODE> if the authentication identity
554       *                           is to be set through some other means.
555       * @param  saslProperties    A set of additional properties that may be needed
556       *                           to process the SASL bind.
557       * @param  requestControls   The set of controls to include the request to the
558       *                           server.
559       * @param  responseControls  A list to hold the set of controls included in
560       *                           the response from the server.
561       *
562       * @return  A message providing additional information about the bind if
563       *          appropriate, or <CODE>null</CODE> if there is no special
564       *          information available.
565       *
566       * @throws  ClientException  If a client-side problem prevents the bind
567       *                           attempt from succeeding.
568       *
569       * @throws  LDAPException  If the bind fails or some other server-side problem
570       *                         occurs during processing.
571       */
572      public String doSASLAnonymous(ASN1OctetString bindDN,
573                         Map<String,List<String>> saslProperties,
574                         ArrayList<LDAPControl> requestControls,
575                         ArrayList<LDAPControl> responseControls)
576             throws ClientException, LDAPException
577      {
578        String trace = null;
579    
580    
581        // Evaluate the properties provided.  The only one we'll allow is the trace
582        // property, but it is not required.
583        if ((saslProperties == null) || saslProperties.isEmpty())
584        {
585          // This is fine because there are no required properties for this
586          // mechanism.
587        }
588        else
589        {
590          Iterator<String> propertyNames = saslProperties.keySet().iterator();
591          while (propertyNames.hasNext())
592          {
593            String name = propertyNames.next();
594            if (name.equalsIgnoreCase(SASL_PROPERTY_TRACE))
595            {
596              // This is acceptable, and we'll take any single value.
597              List<String> values = saslProperties.get(name);
598              Iterator<String> iterator = values.iterator();
599              if (iterator.hasNext())
600              {
601                trace = iterator.next();
602    
603                if (iterator.hasNext())
604                {
605                  Message message = ERR_LDAPAUTH_TRACE_SINGLE_VALUED.get();
606                  throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
607                                            message);
608                }
609              }
610            }
611            else
612            {
613              Message message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
614                  name, SASL_MECHANISM_ANONYMOUS);
615              throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
616                                        message);
617            }
618          }
619        }
620    
621    
622        // Construct the bind request and send it to the server.
623        ASN1OctetString saslCredentials;
624        if (trace == null)
625        {
626          saslCredentials = null;
627        }
628        else
629        {
630          saslCredentials = new ASN1OctetString(trace);
631        }
632    
633        BindRequestProtocolOp bindRequest =
634             new BindRequestProtocolOp(bindDN, SASL_MECHANISM_ANONYMOUS,
635                                       saslCredentials);
636        LDAPMessage requestMessage =
637             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
638                             requestControls);
639    
640        try
641        {
642          writer.writeMessage(requestMessage);
643        }
644        catch (IOException ioe)
645        {
646          Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
647              SASL_MECHANISM_ANONYMOUS, getExceptionMessage(ioe));
648          throw new ClientException(
649                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
650        }
651        catch (Exception e)
652        {
653          Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
654              SASL_MECHANISM_ANONYMOUS, getExceptionMessage(e));
655          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
656                                    message, e);
657        }
658    
659    
660        // Read the response from the server.
661        LDAPMessage responseMessage;
662        try
663        {
664          responseMessage = reader.readMessage();
665          if (responseMessage == null)
666          {
667            Message message =
668                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
669            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
670                                      message);
671          }
672        }
673        catch (IOException ioe)
674        {
675          Message message =
676              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
677          throw new ClientException(
678                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
679        }
680        catch (ASN1Exception ae)
681        {
682          Message message =
683              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
684          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
685                                    message, ae);
686        }
687        catch (LDAPException le)
688        {
689          Message message =
690              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
691          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
692                                    message, le);
693        }
694        catch (Exception e)
695        {
696          Message message =
697              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
698          throw new ClientException(
699                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
700        }
701    
702    
703        // See if there are any controls in the response.  If so, then add them to
704        // the response controls list.
705        ArrayList<LDAPControl> respControls = responseMessage.getControls();
706        if ((respControls != null) && (! respControls.isEmpty()))
707        {
708          responseControls.addAll(respControls);
709        }
710    
711    
712        // Look at the protocol op from the response.  If it's a bind response, then
713        // continue.  If it's an extended response, then it could be a notice of
714        // disconnection so check for that.  Otherwise, generate an error.
715        switch (responseMessage.getProtocolOpType())
716        {
717          case OP_TYPE_BIND_RESPONSE:
718            // We'll deal with this later.
719            break;
720    
721          case OP_TYPE_EXTENDED_RESPONSE:
722            ExtendedResponseProtocolOp extendedResponse =
723                 responseMessage.getExtendedResponseProtocolOp();
724            String responseOID = extendedResponse.getOID();
725            if ((responseOID != null) &&
726                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
727            {
728              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
729                  get(extendedResponse.getResultCode(),
730                      extendedResponse.getErrorMessage());
731              throw new LDAPException(extendedResponse.getResultCode(), message);
732            }
733            else
734            {
735              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
736                  String.valueOf(extendedResponse));
737              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
738                                        message);
739            }
740    
741          default:
742            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
743                String.valueOf(responseMessage.getProtocolOp()));
744            throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
745                                      message);
746        }
747    
748    
749        BindResponseProtocolOp bindResponse =
750             responseMessage.getBindResponseProtocolOp();
751        int resultCode = bindResponse.getResultCode();
752        if (resultCode == LDAPResultCode.SUCCESS)
753        {
754          // FIXME -- Need to look for things like password expiration warning,
755          // reset notice, etc.
756          return null;
757        }
758    
759        // FIXME -- Add support for referrals.
760    
761        Message message =
762            ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_ANONYMOUS);
763        throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
764                                message, bindResponse.getMatchedDN(), null);
765      }
766    
767    
768    
769      /**
770       * Retrieves the set of properties that a client may provide when performing a
771       * SASL ANONYMOUS 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 ANONYMOUS bind, mapped from the property names to their
776       *          corresponding descriptions.
777       */
778      public static LinkedHashMap<String, Message> getSASLAnonymousProperties()
779      {
780        LinkedHashMap<String,Message> properties =
781             new LinkedHashMap<String,Message>(1);
782    
783        properties.put(SASL_PROPERTY_TRACE,
784                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_TRACE.get());
785    
786        return properties;
787      }
788    
789    
790    
791      /**
792       * Processes a SASL CRAM-MD5 bind with the provided information.
793       *
794       * @param  bindDN            The DN to use to bind to the Directory Server, or
795       *                           <CODE>null</CODE> if the authentication identity
796       *                           is to be set through some other means.
797       * @param  bindPassword      The password to use to bind to the Directory
798       *                           Server.
799       * @param  saslProperties    A set of additional properties that may be needed
800       *                           to process the SASL bind.
801       * @param  requestControls   The set of controls to include the request to the
802       *                           server.
803       * @param  responseControls  A list to hold the set of controls included in
804       *                           the response from the server.
805       *
806       * @return  A message providing additional information about the bind if
807       *          appropriate, or <CODE>null</CODE> if there is no special
808       *          information available.
809       *
810       * @throws  ClientException  If a client-side problem prevents the bind
811       *                           attempt from succeeding.
812       *
813       * @throws  LDAPException  If the bind fails or some other server-side problem
814       *                         occurs during processing.
815       */
816      public String doSASLCRAMMD5(ASN1OctetString bindDN,
817                         ASN1OctetString bindPassword,
818                         Map<String,List<String>> saslProperties,
819                         ArrayList<LDAPControl> requestControls,
820                         ArrayList<LDAPControl> responseControls)
821             throws ClientException, LDAPException
822      {
823        String authID  = null;
824    
825    
826        // Evaluate the properties provided.  The authID is required, no other
827        // properties are allowed.
828        if ((saslProperties == null) || saslProperties.isEmpty())
829        {
830          Message message =
831              ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_CRAM_MD5);
832          throw new ClientException(
833                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
834        }
835    
836        Iterator<String> propertyNames = saslProperties.keySet().iterator();
837        while (propertyNames.hasNext())
838        {
839          String name      = propertyNames.next();
840          String lowerName = toLowerCase(name);
841    
842          if (lowerName.equals(SASL_PROPERTY_AUTHID))
843          {
844            List<String> values = saslProperties.get(name);
845            Iterator<String> iterator = values.iterator();
846            if (iterator.hasNext())
847            {
848              authID = iterator.next();
849    
850              if (iterator.hasNext())
851              {
852                Message message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
853                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
854                                          message);
855              }
856            }
857          }
858          else
859          {
860            Message message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
861                name, SASL_MECHANISM_CRAM_MD5);
862            throw new ClientException(
863                    LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
864          }
865        }
866    
867    
868        // Make sure that the authID was provided.
869        if ((authID == null) || (authID.length() == 0))
870        {
871          Message message =
872              ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_CRAM_MD5);
873          throw new ClientException(
874                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
875        }
876    
877    
878        // See if the password was null.  If so, then interactively prompt it from
879        // the user.
880        if (bindPassword == null)
881        {
882          System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(authID));
883          char[] pwChars = PasswordReader.readPassword();
884          if (pwChars == null)
885          {
886            bindPassword = new ASN1OctetString();
887          }
888          else
889          {
890            bindPassword = new ASN1OctetString(getBytes(pwChars));
891            Arrays.fill(pwChars, '\u0000');
892          }
893        }
894    
895    
896        // Construct the initial bind request to send to the server.  In this case,
897        // we'll simply indicate that we want to use CRAM-MD5 so the server will
898        // send us the challenge.
899        BindRequestProtocolOp bindRequest1 =
900             new BindRequestProtocolOp(bindDN, SASL_MECHANISM_CRAM_MD5, null);
901        // FIXME -- Should we include request controls in both stages or just the
902        // second stage?
903        LDAPMessage requestMessage1 =
904             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1);
905    
906        try
907        {
908          writer.writeMessage(requestMessage1);
909        }
910        catch (IOException ioe)
911        {
912          Message message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
913              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
914          throw new ClientException(
915                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
916        }
917        catch (Exception e)
918        {
919          Message message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
920              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
921          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
922                                    message, e);
923        }
924    
925    
926        // Read the response from the server.
927        LDAPMessage responseMessage1;
928        try
929        {
930          responseMessage1 = reader.readMessage();
931          if (responseMessage1 == null)
932          {
933            Message message =
934                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
935            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
936                                      message);
937          }
938        }
939        catch (IOException ioe)
940        {
941          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
942              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
943          throw new ClientException(
944                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
945        }
946        catch (ASN1Exception ae)
947        {
948          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
949              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ae));
950          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
951                                    message, ae);
952        }
953        catch (LDAPException le)
954        {
955          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
956              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(le));
957          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
958                                    message, le);
959        }
960        catch (Exception e)
961        {
962          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
963              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
964          throw new ClientException(
965                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
966        }
967    
968    
969        // Look at the protocol op from the response.  If it's a bind response, then
970        // continue.  If it's an extended response, then it could be a notice of
971        // disconnection so check for that.  Otherwise, generate an error.
972        switch (responseMessage1.getProtocolOpType())
973        {
974          case OP_TYPE_BIND_RESPONSE:
975            // We'll deal with this later.
976            break;
977    
978          case OP_TYPE_EXTENDED_RESPONSE:
979            ExtendedResponseProtocolOp extendedResponse =
980                 responseMessage1.getExtendedResponseProtocolOp();
981            String responseOID = extendedResponse.getOID();
982            if ((responseOID != null) &&
983                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
984            {
985              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
986                  get(extendedResponse.getResultCode(),
987                      extendedResponse.getErrorMessage());
988              throw new LDAPException(extendedResponse.getResultCode(), message);
989            }
990            else
991            {
992              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
993                  String.valueOf(extendedResponse));
994              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
995                                        message);
996            }
997    
998          default:
999            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
1000                String.valueOf(responseMessage1.getProtocolOp()));
1001            throw new ClientException(
1002                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
1003        }
1004    
1005    
1006        // Make sure that the bind response has the "SASL bind in progress" result
1007        // code.
1008        BindResponseProtocolOp bindResponse1 =
1009             responseMessage1.getBindResponseProtocolOp();
1010        int resultCode1 = bindResponse1.getResultCode();
1011        if (resultCode1 != LDAPResultCode.SASL_BIND_IN_PROGRESS)
1012        {
1013          Message errorMessage = bindResponse1.getErrorMessage();
1014          if (errorMessage == null)
1015          {
1016            errorMessage = Message.EMPTY;
1017          }
1018    
1019          Message message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
1020              get(SASL_MECHANISM_CRAM_MD5, resultCode1,
1021                  LDAPResultCode.toString(resultCode1), errorMessage);
1022          throw new LDAPException(resultCode1, errorMessage, message,
1023                                  bindResponse1.getMatchedDN(), null);
1024        }
1025    
1026    
1027        // Make sure that the bind response contains SASL credentials with the
1028        // challenge to use for the next stage of the bind.
1029        ASN1OctetString serverChallenge = bindResponse1.getServerSASLCredentials();
1030        if (serverChallenge == null)
1031        {
1032          Message message = ERR_LDAPAUTH_NO_CRAMMD5_SERVER_CREDENTIALS.get();
1033          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1034        }
1035    
1036    
1037        // Use the provided password and credentials to generate the CRAM-MD5
1038        // response.
1039        StringBuilder buffer = new StringBuilder();
1040        buffer.append(authID);
1041        buffer.append(' ');
1042        buffer.append(generateCRAMMD5Digest(bindPassword, serverChallenge));
1043    
1044    
1045        // Create and send the second bind request to the server.
1046        BindRequestProtocolOp bindRequest2 =
1047             new BindRequestProtocolOp(bindDN, SASL_MECHANISM_CRAM_MD5,
1048                                       new ASN1OctetString(buffer.toString()));
1049        LDAPMessage requestMessage2 =
1050             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2,
1051                             requestControls);
1052    
1053        try
1054        {
1055          writer.writeMessage(requestMessage2);
1056        }
1057        catch (IOException ioe)
1058        {
1059          Message message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
1060              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
1061          throw new ClientException(
1062                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1063        }
1064        catch (Exception e)
1065        {
1066          Message message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
1067              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
1068          throw new ClientException(
1069                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1070        }
1071    
1072    
1073        // Read the response from the server.
1074        LDAPMessage responseMessage2;
1075        try
1076        {
1077          responseMessage2 = reader.readMessage();
1078          if (responseMessage2 == null)
1079          {
1080            Message message =
1081                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
1082            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
1083                                      message);
1084          }
1085        }
1086        catch (IOException ioe)
1087        {
1088          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1089              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
1090          throw new ClientException(
1091                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1092        }
1093        catch (ASN1Exception ae)
1094        {
1095          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1096              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ae));
1097          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
1098                                    message, ae);
1099        }
1100        catch (LDAPException le)
1101        {
1102          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1103              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(le));
1104          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
1105                                    message, le);
1106        }
1107        catch (Exception e)
1108        {
1109          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1110              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
1111          throw new ClientException(
1112                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1113        }
1114    
1115    
1116        // See if there are any controls in the response.  If so, then add them to
1117        // the response controls list.
1118        ArrayList<LDAPControl> respControls = responseMessage2.getControls();
1119        if ((respControls != null) && (! respControls.isEmpty()))
1120        {
1121          responseControls.addAll(respControls);
1122        }
1123    
1124    
1125        // Look at the protocol op from the response.  If it's a bind response, then
1126        // continue.  If it's an extended response, then it could be a notice of
1127        // disconnection so check for that.  Otherwise, generate an error.
1128        switch (responseMessage2.getProtocolOpType())
1129        {
1130          case OP_TYPE_BIND_RESPONSE:
1131            // We'll deal with this later.
1132            break;
1133    
1134          case OP_TYPE_EXTENDED_RESPONSE:
1135            ExtendedResponseProtocolOp extendedResponse =
1136                 responseMessage2.getExtendedResponseProtocolOp();
1137            String responseOID = extendedResponse.getOID();
1138            if ((responseOID != null) &&
1139                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
1140            {
1141              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
1142                  get(extendedResponse.getResultCode(),
1143                      extendedResponse.getErrorMessage());
1144              throw new LDAPException(extendedResponse.getResultCode(), message);
1145            }
1146            else
1147            {
1148              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
1149                  String.valueOf(extendedResponse));
1150              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
1151                                        message);
1152            }
1153    
1154          default:
1155            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
1156                String.valueOf(responseMessage2.getProtocolOp()));
1157            throw new ClientException(
1158                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
1159        }
1160    
1161    
1162        BindResponseProtocolOp bindResponse2 =
1163             responseMessage2.getBindResponseProtocolOp();
1164        int resultCode2 = bindResponse2.getResultCode();
1165        if (resultCode2 == LDAPResultCode.SUCCESS)
1166        {
1167          // FIXME -- Need to look for things like password expiration warning,
1168          // reset notice, etc.
1169          return null;
1170        }
1171    
1172        // FIXME -- Add support for referrals.
1173    
1174        Message message =
1175            ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_CRAM_MD5);
1176        throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(),
1177                                message, bindResponse2.getMatchedDN(), null);
1178      }
1179    
1180    
1181    
1182      /**
1183       * Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication
1184       * with the given information.
1185       *
1186       * @param  password   The clear-text password to use when generating the
1187       *                    digest.
1188       * @param  challenge  The server-supplied challenge to use when generating the
1189       *                    digest.
1190       *
1191       * @return  The generated HMAC-MD5 digest for CRAM-MD5 authentication.
1192       *
1193       * @throws  ClientException  If a problem occurs while attempting to perform
1194       *                           the necessary initialization.
1195       */
1196      private String generateCRAMMD5Digest(ASN1OctetString password,
1197                                           ASN1OctetString challenge)
1198              throws ClientException
1199      {
1200        // Perform the necessary initialization if it hasn't been done yet.
1201        if (md5Digest == null)
1202        {
1203          try
1204          {
1205            md5Digest = MessageDigest.getInstance("MD5");
1206          }
1207          catch (Exception e)
1208          {
1209            Message message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
1210                getExceptionMessage(e));
1211            throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
1212                    message, e);
1213          }
1214        }
1215    
1216        if (iPad == null)
1217        {
1218          iPad = new byte[HMAC_MD5_BLOCK_LENGTH];
1219          oPad = new byte[HMAC_MD5_BLOCK_LENGTH];
1220          Arrays.fill(iPad, CRAMMD5_IPAD_BYTE);
1221          Arrays.fill(oPad, CRAMMD5_OPAD_BYTE);
1222        }
1223    
1224    
1225        // Get the byte arrays backing the password and challenge.
1226        byte[] p = password.value();
1227        byte[] c = challenge.value();
1228    
1229    
1230        // If the password is longer than the HMAC-MD5 block length, then use an
1231        // MD5 digest of the password rather than the password itself.
1232        if (p.length > HMAC_MD5_BLOCK_LENGTH)
1233        {
1234          p = md5Digest.digest(p);
1235        }
1236    
1237    
1238        // Create byte arrays with data needed for the hash generation.
1239        byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length];
1240        System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH);
1241        System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length);
1242    
1243        byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH];
1244        System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH);
1245    
1246    
1247        // Iterate through the bytes in the key and XOR them with the iPad and
1248        // oPad as appropriate.
1249        for (int i=0; i < p.length; i++)
1250        {
1251          iPadAndData[i] ^= p[i];
1252          oPadAndHash[i] ^= p[i];
1253        }
1254    
1255    
1256        // Copy an MD5 digest of the iPad-XORed key and the data into the array to
1257        // be hashed.
1258        System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash,
1259                         HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH);
1260    
1261    
1262        // Calculate an MD5 digest of the resulting array and get the corresponding
1263        // hex string representation.
1264        byte[] digestBytes = md5Digest.digest(oPadAndHash);
1265    
1266        StringBuilder hexDigest = new StringBuilder(2*digestBytes.length);
1267        for (byte b : digestBytes)
1268        {
1269          hexDigest.append(byteToLowerHex(b));
1270        }
1271    
1272        return hexDigest.toString();
1273      }
1274    
1275    
1276    
1277      /**
1278       * Retrieves the set of properties that a client may provide when performing a
1279       * SASL CRAM-MD5 bind, mapped from the property names to their corresponding
1280       * descriptions.
1281       *
1282       * @return  The set of properties that a client may provide when performing a
1283       *          SASL CRAM-MD5 bind, mapped from the property names to their
1284       *          corresponding descriptions.
1285       */
1286      public static LinkedHashMap<String,Message> getSASLCRAMMD5Properties()
1287      {
1288        LinkedHashMap<String,Message> properties =
1289             new LinkedHashMap<String,Message>(1);
1290    
1291        properties.put(SASL_PROPERTY_AUTHID,
1292                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
1293    
1294        return properties;
1295      }
1296    
1297    
1298    
1299      /**
1300       * Processes a SASL DIGEST-MD5 bind with the provided information.
1301       *
1302       * @param  bindDN            The DN to use to bind to the Directory Server, or
1303       *                           <CODE>null</CODE> if the authentication identity
1304       *                           is to be set through some other means.
1305       * @param  bindPassword      The password to use to bind to the Directory
1306       *                           Server.
1307       * @param  saslProperties    A set of additional properties that may be needed
1308       *                           to process the SASL bind.
1309       * @param  requestControls   The set of controls to include the request to the
1310       *                           server.
1311       * @param  responseControls  A list to hold the set of controls included in
1312       *                           the response from the server.
1313       *
1314       * @return  A message providing additional information about the bind if
1315       *          appropriate, or <CODE>null</CODE> if there is no special
1316       *          information available.
1317       *
1318       * @throws  ClientException  If a client-side problem prevents the bind
1319       *                           attempt from succeeding.
1320       *
1321       * @throws  LDAPException  If the bind fails or some other server-side problem
1322       *                         occurs during processing.
1323       */
1324      public String doSASLDigestMD5(ASN1OctetString bindDN,
1325                         ASN1OctetString bindPassword,
1326                         Map<String,List<String>> saslProperties,
1327                         ArrayList<LDAPControl> requestControls,
1328                         ArrayList<LDAPControl> responseControls)
1329             throws ClientException, LDAPException
1330      {
1331        String  authID               = null;
1332        String  realm                = null;
1333        String  qop                  = "auth";
1334        String  digestURI            = "ldap/" + hostName;
1335        String  authzID              = null;
1336        boolean realmSetFromProperty = false;
1337    
1338    
1339        // Evaluate the properties provided.  The authID is required.  The realm,
1340        // QoP, digest URI, and authzID are optional.
1341        if ((saslProperties == null) || saslProperties.isEmpty())
1342        {
1343          Message message =
1344              ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_DIGEST_MD5);
1345          throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1346                  message);
1347        }
1348    
1349        Iterator<String> propertyNames = saslProperties.keySet().iterator();
1350        while (propertyNames.hasNext())
1351        {
1352          String name      = propertyNames.next();
1353          String lowerName = toLowerCase(name);
1354    
1355          if (lowerName.equals(SASL_PROPERTY_AUTHID))
1356          {
1357            List<String> values = saslProperties.get(name);
1358            Iterator<String> iterator = values.iterator();
1359            if (iterator.hasNext())
1360            {
1361              authID = iterator.next();
1362    
1363              if (iterator.hasNext())
1364              {
1365                Message message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
1366                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1367                                          message);
1368              }
1369            }
1370          }
1371          else if (lowerName.equals(SASL_PROPERTY_REALM))
1372          {
1373            List<String> values = saslProperties.get(name);
1374            Iterator<String> iterator = values.iterator();
1375            if (iterator.hasNext())
1376            {
1377              realm                = iterator.next();
1378              realmSetFromProperty = true;
1379    
1380              if (iterator.hasNext())
1381              {
1382                Message message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get();
1383                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1384                                          message);
1385              }
1386            }
1387          }
1388          else if (lowerName.equals(SASL_PROPERTY_QOP))
1389          {
1390            List<String> values = saslProperties.get(name);
1391            Iterator<String> iterator = values.iterator();
1392            if (iterator.hasNext())
1393            {
1394              qop = toLowerCase(iterator.next());
1395    
1396              if (iterator.hasNext())
1397              {
1398                Message message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
1399                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1400                                          message);
1401              }
1402    
1403              if (qop.equals("auth"))
1404              {
1405                // This is always fine.
1406              }
1407              else if (qop.equals("auth-int") || qop.equals("auth-conf"))
1408              {
1409                // FIXME -- Add support for integrity and confidentiality.
1410                Message message = ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(qop);
1411                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1412                                          message);
1413              }
1414              else
1415              {
1416                // This is an illegal value.
1417                Message message = ERR_LDAPAUTH_DIGESTMD5_INVALID_QOP.get(qop);
1418                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1419                                          message);
1420              }
1421            }
1422          }
1423          else if (lowerName.equals(SASL_PROPERTY_DIGEST_URI))
1424          {
1425            List<String> values = saslProperties.get(name);
1426            Iterator<String> iterator = values.iterator();
1427            if (iterator.hasNext())
1428            {
1429              digestURI = toLowerCase(iterator.next());
1430    
1431              if (iterator.hasNext())
1432              {
1433                Message message = ERR_LDAPAUTH_DIGEST_URI_SINGLE_VALUED.get();
1434                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1435                                          message);
1436              }
1437            }
1438          }
1439          else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
1440          {
1441            List<String> values = saslProperties.get(name);
1442            Iterator<String> iterator = values.iterator();
1443            if (iterator.hasNext())
1444            {
1445              authzID = toLowerCase(iterator.next());
1446    
1447              if (iterator.hasNext())
1448              {
1449                Message message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
1450                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1451                                          message);
1452              }
1453            }
1454          }
1455          else
1456          {
1457            Message message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
1458                name, SASL_MECHANISM_DIGEST_MD5);
1459            throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1460                    message);
1461          }
1462        }
1463    
1464    
1465        // Make sure that the authID was provided.
1466        if ((authID == null) || (authID.length() == 0))
1467        {
1468          Message message =
1469              ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_DIGEST_MD5);
1470          throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1471                  message);
1472        }
1473    
1474    
1475        // See if the password was null.  If so, then interactively prompt it from
1476        // the user.
1477        if (bindPassword == null)
1478        {
1479          System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(authID));
1480          char[] pwChars = PasswordReader.readPassword();
1481          if (pwChars == null)
1482          {
1483            bindPassword = new ASN1OctetString();
1484          }
1485          else
1486          {
1487            bindPassword = new ASN1OctetString(getBytes(pwChars));
1488            Arrays.fill(pwChars, '\u0000');
1489          }
1490        }
1491    
1492    
1493        // Construct the initial bind request to send to the server.  In this case,
1494        // we'll simply indicate that we want to use DIGEST-MD5 so the server will
1495        // send us the challenge.
1496        BindRequestProtocolOp bindRequest1 =
1497             new BindRequestProtocolOp(bindDN, SASL_MECHANISM_DIGEST_MD5, null);
1498        // FIXME -- Should we include request controls in both stages or just the
1499        // second stage?
1500        LDAPMessage requestMessage1 =
1501             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1);
1502    
1503        try
1504        {
1505          writer.writeMessage(requestMessage1);
1506        }
1507        catch (IOException ioe)
1508        {
1509          Message message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
1510              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1511          throw new ClientException(
1512                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1513        }
1514        catch (Exception e)
1515        {
1516          Message message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
1517              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1518          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
1519                                    message, e);
1520        }
1521    
1522    
1523        // Read the response from the server.
1524        LDAPMessage responseMessage1;
1525        try
1526        {
1527          responseMessage1 = reader.readMessage();
1528          if (responseMessage1 == null)
1529          {
1530            Message message =
1531                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
1532            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
1533                                      message);
1534          }
1535        }
1536        catch (IOException ioe)
1537        {
1538          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
1539              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1540          throw new ClientException(
1541                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1542        }
1543        catch (ASN1Exception ae)
1544        {
1545          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
1546              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ae));
1547          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
1548                                    message, ae);
1549        }
1550        catch (LDAPException le)
1551        {
1552          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
1553              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(le));
1554          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
1555                                    message, le);
1556        }
1557        catch (Exception e)
1558        {
1559          Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
1560              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1561          throw new ClientException(
1562                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1563        }
1564    
1565    
1566        // Look at the protocol op from the response.  If it's a bind response, then
1567        // continue.  If it's an extended response, then it could be a notice of
1568        // disconnection so check for that.  Otherwise, generate an error.
1569        switch (responseMessage1.getProtocolOpType())
1570        {
1571          case OP_TYPE_BIND_RESPONSE:
1572            // We'll deal with this later.
1573            break;
1574    
1575          case OP_TYPE_EXTENDED_RESPONSE:
1576            ExtendedResponseProtocolOp extendedResponse =
1577                 responseMessage1.getExtendedResponseProtocolOp();
1578            String responseOID = extendedResponse.getOID();
1579            if ((responseOID != null) &&
1580                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
1581            {
1582              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
1583                  get(extendedResponse.getResultCode(),
1584                      extendedResponse.getErrorMessage());
1585              throw new LDAPException(extendedResponse.getResultCode(), message);
1586            }
1587            else
1588            {
1589              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
1590                  String.valueOf(extendedResponse));
1591              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
1592                                        message);
1593            }
1594    
1595          default:
1596            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
1597                String.valueOf(responseMessage1.getProtocolOp()));
1598            throw new ClientException(
1599                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
1600        }
1601    
1602    
1603        // Make sure that the bind response has the "SASL bind in progress" result
1604        // code.
1605        BindResponseProtocolOp bindResponse1 =
1606             responseMessage1.getBindResponseProtocolOp();
1607        int resultCode1 = bindResponse1.getResultCode();
1608        if (resultCode1 != LDAPResultCode.SASL_BIND_IN_PROGRESS)
1609        {
1610          Message errorMessage = bindResponse1.getErrorMessage();
1611          if (errorMessage == null)
1612          {
1613            errorMessage = Message.EMPTY;
1614          }
1615    
1616          Message message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
1617              get(SASL_MECHANISM_DIGEST_MD5, resultCode1,
1618                  LDAPResultCode.toString(resultCode1), errorMessage);
1619          throw new LDAPException(resultCode1, errorMessage, message,
1620                                  bindResponse1.getMatchedDN(), null);
1621        }
1622    
1623    
1624        // Make sure that the bind response contains SASL credentials with the
1625        // information to use for the next stage of the bind.
1626        ASN1OctetString serverCredentials =
1627             bindResponse1.getServerSASLCredentials();
1628        if (serverCredentials == null)
1629        {
1630          Message message = ERR_LDAPAUTH_NO_DIGESTMD5_SERVER_CREDENTIALS.get();
1631          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1632        }
1633    
1634    
1635        // Parse the server SASL credentials to get the necessary information.  In
1636        // particular, look at the realm, the nonce, the QoP modes, and the charset.
1637        // We'll only care about the realm if none was provided in the SASL
1638        // properties and only one was provided in the server SASL credentials.
1639        String  credString = serverCredentials.stringValue();
1640        String  lowerCreds = toLowerCase(credString);
1641        String  nonce      = null;
1642        boolean useUTF8    = false;
1643        int     pos        = 0;
1644        int     length     = credString.length();
1645        while (pos < length)
1646        {
1647          int equalPos = credString.indexOf('=', pos+1);
1648          if (equalPos < 0)
1649          {
1650            // This is bad because we're not at the end of the string but we don't
1651            // have a name/value delimiter.
1652            Message message =
1653                ERR_LDAPAUTH_DIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get(
1654                        credString, pos);
1655            throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1656          }
1657    
1658    
1659          String tokenName  = lowerCreds.substring(pos, equalPos);
1660    
1661          StringBuilder valueBuffer = new StringBuilder();
1662          pos = readToken(credString, equalPos+1, length, valueBuffer);
1663          String tokenValue = valueBuffer.toString();
1664    
1665          if (tokenName.equals("charset"))
1666          {
1667            // The value must be the string "utf-8".  If not, that's an error.
1668            if (! tokenValue.equalsIgnoreCase("utf-8"))
1669            {
1670              Message message =
1671                  ERR_LDAPAUTH_DIGESTMD5_INVALID_CHARSET.get(tokenValue);
1672              throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1673            }
1674    
1675            useUTF8 = true;
1676          }
1677          else if (tokenName.equals("realm"))
1678          {
1679            // This will only be of interest to us if there is only a single realm
1680            // in the server credentials and none was provided as a client-side
1681            // property.
1682            if (! realmSetFromProperty)
1683            {
1684              if (realm == null)
1685              {
1686                // No other realm was specified, so we'll use this one for now.
1687                realm = tokenValue;
1688              }
1689              else
1690              {
1691                // This must mean that there are multiple realms in the server
1692                // credentials.  In that case, we'll not provide any realm at all.
1693                // To make sure that happens, pretend that the client specified the
1694                // realm.
1695                realm                = null;
1696                realmSetFromProperty = true;
1697              }
1698            }
1699          }
1700          else if (tokenName.equals("nonce"))
1701          {
1702            nonce = tokenValue;
1703          }
1704          else if (tokenName.equals("qop"))
1705          {
1706            // The QoP modes provided by the server should be a comma-delimited
1707            // list.  Decode that list and make sure the QoP we have chosen is in
1708            // that list.
1709            StringTokenizer tokenizer = new StringTokenizer(tokenValue, ",");
1710            LinkedList<String> qopModes = new LinkedList<String>();
1711            while (tokenizer.hasMoreTokens())
1712            {
1713              qopModes.add(toLowerCase(tokenizer.nextToken().trim()));
1714            }
1715    
1716            if (! qopModes.contains(qop))
1717            {
1718              Message message = ERR_LDAPAUTH_REQUESTED_QOP_NOT_SUPPORTED_BY_SERVER.
1719                  get(qop, tokenValue);
1720              throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
1721                                        message);
1722            }
1723          }
1724          else
1725          {
1726            // Other values may have been provided, but they aren't of interest to
1727            // us because they shouldn't change anything about the way we encode the
1728            // second part of the request.  Rather than attempt to examine them,
1729            // we'll assume that the server sent a valid response.
1730          }
1731        }
1732    
1733    
1734        // Make sure that the nonce was included in the response from the server.
1735        if (nonce == null)
1736        {
1737          Message message = ERR_LDAPAUTH_DIGESTMD5_NO_NONCE.get();
1738          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1739        }
1740    
1741    
1742        // Generate the cnonce that we will use for this request.
1743        String cnonce = generateCNonce();
1744    
1745    
1746        // Generate the response digest, and initialize the necessary remaining
1747        // variables to use in the generation of that digest.
1748        String nonceCount = "00000001";
1749        String charset    = (useUTF8 ? "UTF-8" : "ISO-8859-1");
1750        String responseDigest;
1751        try
1752        {
1753          responseDigest = generateDigestMD5Response(authID, authzID,
1754                                                     bindPassword.value(), realm,
1755                                                     nonce, cnonce, nonceCount,
1756                                                     digestURI, qop, charset);
1757        }
1758        catch (ClientException ce)
1759        {
1760          throw ce;
1761        }
1762        catch (Exception e)
1763        {
1764          Message message = ERR_LDAPAUTH_DIGESTMD5_CANNOT_CREATE_RESPONSE_DIGEST.
1765              get(getExceptionMessage(e));
1766          throw new ClientException(
1767                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1768        }
1769    
1770    
1771        // Generate the SASL credentials for the second bind request.
1772        StringBuilder credBuffer = new StringBuilder();
1773        credBuffer.append("username=\"");
1774        credBuffer.append(authID);
1775        credBuffer.append("\"");
1776    
1777        if (realm != null)
1778        {
1779          credBuffer.append(",realm=\"");
1780          credBuffer.append(realm);
1781          credBuffer.append("\"");
1782        }
1783    
1784        credBuffer.append(",nonce=\"");
1785        credBuffer.append(nonce);
1786        credBuffer.append("\",cnonce=\"");
1787        credBuffer.append(cnonce);
1788        credBuffer.append("\",nc=");
1789        credBuffer.append(nonceCount);
1790        credBuffer.append(",qop=");
1791        credBuffer.append(qop);
1792        credBuffer.append(",digest-uri=\"");
1793        credBuffer.append(digestURI);
1794        credBuffer.append("\",response=");
1795        credBuffer.append(responseDigest);
1796    
1797        if (useUTF8)
1798        {
1799          credBuffer.append(",charset=utf-8");
1800        }
1801    
1802        if (authzID != null)
1803        {
1804          credBuffer.append(",authzid=\"");
1805          credBuffer.append(authzID);
1806          credBuffer.append("\"");
1807        }
1808    
1809    
1810        // Generate and send the second bind request.
1811        BindRequestProtocolOp bindRequest2 =
1812             new BindRequestProtocolOp(bindDN, SASL_MECHANISM_DIGEST_MD5,
1813                                       new ASN1OctetString(credBuffer.toString()));
1814        LDAPMessage requestMessage2 =
1815             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2,
1816                             requestControls);
1817    
1818        try
1819        {
1820          writer.writeMessage(requestMessage2);
1821        }
1822        catch (IOException ioe)
1823        {
1824          Message message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
1825              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1826          throw new ClientException(
1827                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1828        }
1829        catch (Exception e)
1830        {
1831          Message message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
1832              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1833          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
1834                                    message, e);
1835        }
1836    
1837    
1838        // Read the response from the server.
1839        LDAPMessage responseMessage2;
1840        try
1841        {
1842          responseMessage2 = reader.readMessage();
1843          if (responseMessage2 == null)
1844          {
1845            Message message =
1846                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
1847            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
1848                                      message);
1849          }
1850        }
1851        catch (IOException ioe)
1852        {
1853          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1854              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1855          throw new ClientException(
1856                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1857        }
1858        catch (ASN1Exception ae)
1859        {
1860          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1861              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ae));
1862          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
1863                                    message, ae);
1864        }
1865        catch (LDAPException le)
1866        {
1867          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1868              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(le));
1869          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
1870                                    message, le);
1871        }
1872        catch (Exception e)
1873        {
1874          Message message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1875              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1876          throw new ClientException(
1877                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1878        }
1879    
1880    
1881        // See if there are any controls in the response.  If so, then add them to
1882        // the response controls list.
1883        ArrayList<LDAPControl> respControls = responseMessage2.getControls();
1884        if ((respControls != null) && (! respControls.isEmpty()))
1885        {
1886          responseControls.addAll(respControls);
1887        }
1888    
1889    
1890        // Look at the protocol op from the response.  If it's a bind response, then
1891        // continue.  If it's an extended response, then it could be a notice of
1892        // disconnection so check for that.  Otherwise, generate an error.
1893        switch (responseMessage2.getProtocolOpType())
1894        {
1895          case OP_TYPE_BIND_RESPONSE:
1896            // We'll deal with this later.
1897            break;
1898    
1899          case OP_TYPE_EXTENDED_RESPONSE:
1900            ExtendedResponseProtocolOp extendedResponse =
1901                 responseMessage2.getExtendedResponseProtocolOp();
1902            String responseOID = extendedResponse.getOID();
1903            if ((responseOID != null) &&
1904                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
1905            {
1906              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
1907                  get(extendedResponse.getResultCode(),
1908                      extendedResponse.getErrorMessage());
1909              throw new LDAPException(extendedResponse.getResultCode(), message);
1910            }
1911            else
1912            {
1913              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
1914                  String.valueOf(extendedResponse));
1915              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
1916                                        message);
1917            }
1918    
1919          default:
1920            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
1921                String.valueOf(responseMessage2.getProtocolOp()));
1922            throw new ClientException(
1923                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
1924        }
1925    
1926    
1927        BindResponseProtocolOp bindResponse2 =
1928             responseMessage2.getBindResponseProtocolOp();
1929        int resultCode2 = bindResponse2.getResultCode();
1930        if (resultCode2 != LDAPResultCode.SUCCESS)
1931        {
1932          // FIXME -- Add support for referrals.
1933    
1934          Message message =
1935              ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_DIGEST_MD5);
1936          throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(),
1937                                  message, bindResponse2.getMatchedDN(),
1938                                  null);
1939        }
1940    
1941    
1942        // Make sure that the bind response included server SASL credentials with
1943        // the appropriate rspauth value.
1944        ASN1OctetString rspAuthCreds = bindResponse2.getServerSASLCredentials();
1945        if (rspAuthCreds == null)
1946        {
1947          Message message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
1948          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1949        }
1950    
1951        String credStr = toLowerCase(rspAuthCreds.stringValue());
1952        if (! credStr.startsWith("rspauth="))
1953        {
1954          Message message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
1955          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1956        }
1957    
1958    
1959        byte[] serverRspAuth;
1960        try
1961        {
1962          serverRspAuth = hexStringToByteArray(credStr.substring(8));
1963        }
1964        catch (Exception e)
1965        {
1966          Message message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_DECODE_RSPAUTH.get(
1967              getExceptionMessage(e));
1968          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
1969        }
1970    
1971        byte[] clientRspAuth;
1972        try
1973        {
1974          clientRspAuth =
1975               generateDigestMD5RspAuth(authID, authzID, bindPassword.value(),
1976                                        realm, nonce, cnonce, nonceCount, digestURI,
1977                                        qop, charset);
1978        }
1979        catch (Exception e)
1980        {
1981          Message message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_CALCULATE_RSPAUTH.get(
1982              getExceptionMessage(e));
1983          throw new ClientException(
1984                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
1985        }
1986    
1987        if (! Arrays.equals(serverRspAuth, clientRspAuth))
1988        {
1989          Message message = ERR_LDAPAUTH_DIGESTMD5_RSPAUTH_MISMATCH.get();
1990          throw new ClientException(
1991                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
1992        }
1993    
1994        // FIXME -- Need to look for things like password expiration warning,
1995        // reset notice, etc.
1996        return null;
1997      }
1998    
1999    
2000    
2001      /**
2002       * Reads the next token from the provided credentials string using the
2003       * provided information.  If the token is surrounded by quotation marks, then
2004       * the token returned will not include those quotation marks.
2005       *
2006       * @param  credentials  The credentials string from which to read the token.
2007       * @param  startPos     The position of the first character of the token to
2008       *                      read.
2009       * @param  length       The total number of characters in the credentials
2010       *                      string.
2011       * @param  token        The buffer into which the token is to be placed.
2012       *
2013       * @return  The position at which the next token should start, or a value
2014       *          greater than or equal to the length of the string if there are no
2015       *          more tokens.
2016       *
2017       * @throws  LDAPException  If a problem occurs while attempting to read the
2018       *                         token.
2019       */
2020      private int readToken(String credentials, int startPos, int length,
2021                            StringBuilder token)
2022              throws LDAPException
2023      {
2024        // If the position is greater than or equal to the length, then we shouldn't
2025        // do anything.
2026        if (startPos >= length)
2027        {
2028          return startPos;
2029        }
2030    
2031    
2032        // Look at the first character to see if it's an empty string or the string
2033        // is quoted.
2034        boolean isEscaped = false;
2035        boolean isQuoted  = false;
2036        int     pos       = startPos;
2037        char    c         = credentials.charAt(pos++);
2038    
2039        if (c == ',')
2040        {
2041          // This must be a zero-length token, so we'll just return the next
2042          // position.
2043          return pos;
2044        }
2045        else if (c == '"')
2046        {
2047          // The string is quoted, so we'll ignore this character, and we'll keep
2048          // reading until we find the unescaped closing quote followed by a comma
2049          // or the end of the string.
2050          isQuoted = true;
2051        }
2052        else if (c == '\\')
2053        {
2054          // The next character is escaped, so we'll take it no matter what.
2055          isEscaped = true;
2056        }
2057        else
2058        {
2059          // The string is not quoted, and this is the first character.  Store this
2060          // character and keep reading until we find a comma or the end of the
2061          // string.
2062          token.append(c);
2063        }
2064    
2065    
2066        // Enter a loop, reading until we find the appropriate criteria for the end
2067        // of the token.
2068        while (pos < length)
2069        {
2070          c = credentials.charAt(pos++);
2071    
2072          if (isEscaped)
2073          {
2074            // The previous character was an escape, so we'll take this no matter
2075            // what.
2076            token.append(c);
2077            isEscaped = false;
2078          }
2079          else if (c == ',')
2080          {
2081            // If this is a quoted string, then this comma is part of the token.
2082            // Otherwise, it's the end of the token.
2083            if (isQuoted)
2084            {
2085              token.append(c);
2086            }
2087            else
2088            {
2089              break;
2090            }
2091          }
2092          else if (c == '"')
2093          {
2094            if (isQuoted)
2095            {
2096              // This should be the end of the token, but in order for it to be
2097              // valid it must be followed by a comma or the end of the string.
2098              if (pos >= length)
2099              {
2100                // We have hit the end of the string, so this is fine.
2101                break;
2102              }
2103              else
2104              {
2105                char c2 = credentials.charAt(pos++);
2106                if (c2 == ',')
2107                {
2108                  // We have hit the end of the token, so this is fine.
2109                  break;
2110                }
2111                else
2112                {
2113                  // We found the closing quote before the end of the token.  This
2114                  // is not fine.
2115                  Message message =
2116                      ERR_LDAPAUTH_DIGESTMD5_INVALID_CLOSING_QUOTE_POS.get((pos-2));
2117                  throw new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
2118                                          message);
2119                }
2120              }
2121            }
2122            else
2123            {
2124              // This must be part of the value, so we'll take it.
2125              token.append(c);
2126            }
2127          }
2128          else if (c == '\\')
2129          {
2130            // The next character is escaped.  We'll set a flag so we know to
2131            // accept it, but will not include the backspace itself.
2132            isEscaped = true;
2133          }
2134          else
2135          {
2136            token.append(c);
2137          }
2138        }
2139    
2140    
2141        return pos;
2142      }
2143    
2144    
2145    
2146      /**
2147       * Generates a cnonce value to use during the DIGEST-MD5 authentication
2148       * process.
2149       *
2150       * @return  The cnonce that should be used for DIGEST-MD5 authentication.
2151       */
2152      private String generateCNonce()
2153      {
2154        if (secureRandom == null)
2155        {
2156          secureRandom = new SecureRandom();
2157        }
2158    
2159        byte[] cnonceBytes = new byte[16];
2160        secureRandom.nextBytes(cnonceBytes);
2161    
2162        return Base64.encode(cnonceBytes);
2163      }
2164    
2165    
2166    
2167      /**
2168       * Generates the appropriate DIGEST-MD5 response for the provided set of
2169       * information.
2170       *
2171       * @param  authID    The username from the authentication request.
2172       * @param  authzID     The authorization ID from the request, or
2173       *                     <CODE>null</CODE> if there is none.
2174       * @param  password    The clear-text password for the user.
2175       * @param  realm       The realm for which the authentication is to be
2176       *                     performed.
2177       * @param  nonce       The random data generated by the server for use in the
2178       *                     digest.
2179       * @param  cnonce      The random data generated by the client for use in the
2180       *                     digest.
2181       * @param  nonceCount  The 8-digit hex string indicating the number of times
2182       *                     the provided nonce has been used by the client.
2183       * @param  digestURI   The digest URI that specifies the service and host for
2184       *                     which the authentication is being performed.
2185       * @param  qop         The quality of protection string for the
2186       *                     authentication.
2187       * @param  charset     The character set used to encode the information.
2188       *
2189       * @return  The DIGEST-MD5 response for the provided set of information.
2190       *
2191       * @throws  ClientException  If a problem occurs while attempting to
2192       *                           initialize the MD5 digest.
2193       *
2194       * @throws  UnsupportedEncodingException  If the specified character set is
2195       *                                        invalid for some reason.
2196       */
2197      private String generateDigestMD5Response(String authID, String authzID,
2198                                               byte[] password, String realm,
2199                                               String nonce, String cnonce,
2200                                               String nonceCount, String digestURI,
2201                                               String qop, String charset)
2202              throws ClientException, UnsupportedEncodingException
2203      {
2204        // Perform the necessary initialization if it hasn't been done yet.
2205        if (md5Digest == null)
2206        {
2207          try
2208          {
2209            md5Digest = MessageDigest.getInstance("MD5");
2210          }
2211          catch (Exception e)
2212          {
2213            Message message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
2214                getExceptionMessage(e));
2215            throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
2216                    message, e);
2217          }
2218        }
2219    
2220    
2221        // Get a hash of "username:realm:password".
2222        StringBuilder a1String1 = new StringBuilder();
2223        a1String1.append(authID);
2224        a1String1.append(':');
2225        a1String1.append((realm == null) ? "" : realm);
2226        a1String1.append(':');
2227    
2228        byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
2229        byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length];
2230        System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
2231        System.arraycopy(password, 0, a1Bytes1, a1Bytes1a.length, password.length);
2232        byte[] urpHash = md5Digest.digest(a1Bytes1);
2233    
2234    
2235        // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
2236        StringBuilder a1String2 = new StringBuilder();
2237        a1String2.append(':');
2238        a1String2.append(nonce);
2239        a1String2.append(':');
2240        a1String2.append(cnonce);
2241        if (authzID != null)
2242        {
2243          a1String2.append(':');
2244          a1String2.append(authzID);
2245        }
2246        byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
2247        byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
2248        System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
2249        System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length);
2250        byte[] a1Hash = md5Digest.digest(a1Bytes2);
2251    
2252    
2253        // Next, get a hash of "AUTHENTICATE:digesturi".
2254        byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset);
2255        byte[] a2Hash  = md5Digest.digest(a2Bytes);
2256    
2257    
2258        // Get hex string representations of the last two hashes.
2259        String a1HashHex = getHexString(a1Hash);
2260        String a2HashHex = getHexString(a2Hash);
2261    
2262    
2263        // Put together the final string to hash, consisting of
2264        // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
2265        StringBuilder kdStr = new StringBuilder();
2266        kdStr.append(a1HashHex);
2267        kdStr.append(':');
2268        kdStr.append(nonce);
2269        kdStr.append(':');
2270        kdStr.append(nonceCount);
2271        kdStr.append(':');
2272        kdStr.append(cnonce);
2273        kdStr.append(':');
2274        kdStr.append(qop);
2275        kdStr.append(':');
2276        kdStr.append(a2HashHex);
2277    
2278        return getHexString(md5Digest.digest(kdStr.toString().getBytes(charset)));
2279      }
2280    
2281    
2282    
2283      /**
2284       * Generates the appropriate DIGEST-MD5 rspauth digest using the provided
2285       * information.
2286       *
2287       * @param  authID      The username from the authentication request.
2288       * @param  authzID     The authorization ID from the request, or
2289       *                     <CODE>null</CODE> if there is none.
2290       * @param  password    The clear-text password for the user.
2291       * @param  realm       The realm for which the authentication is to be
2292       *                     performed.
2293       * @param  nonce       The random data generated by the server for use in the
2294       *                     digest.
2295       * @param  cnonce      The random data generated by the client for use in the
2296       *                     digest.
2297       * @param  nonceCount  The 8-digit hex string indicating the number of times
2298       *                     the provided nonce has been used by the client.
2299       * @param  digestURI   The digest URI that specifies the service and host for
2300       *                     which the authentication is being performed.
2301       * @param  qop         The quality of protection string for the
2302       *                     authentication.
2303       * @param  charset     The character set used to encode the information.
2304       *
2305       * @return  The DIGEST-MD5 response for the provided set of information.
2306       *
2307       * @throws  UnsupportedEncodingException  If the specified character set is
2308       *                                        invalid for some reason.
2309       */
2310      public byte[] generateDigestMD5RspAuth(String authID, String authzID,
2311                                             byte[] password, String realm,
2312                                             String nonce, String cnonce,
2313                                             String nonceCount, String digestURI,
2314                                             String qop, String charset)
2315             throws UnsupportedEncodingException
2316      {
2317        // First, get a hash of "username:realm:password".
2318        StringBuilder a1String1 = new StringBuilder();
2319        a1String1.append(authID);
2320        a1String1.append(':');
2321        a1String1.append(realm);
2322        a1String1.append(':');
2323    
2324        byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
2325        byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length];
2326        System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
2327        System.arraycopy(password, 0, a1Bytes1, a1Bytes1a.length,
2328                         password.length);
2329        byte[] urpHash = md5Digest.digest(a1Bytes1);
2330    
2331    
2332        // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
2333        StringBuilder a1String2 = new StringBuilder();
2334        a1String2.append(':');
2335        a1String2.append(nonce);
2336        a1String2.append(':');
2337        a1String2.append(cnonce);
2338        if (authzID != null)
2339        {
2340          a1String2.append(':');
2341          a1String2.append(authzID);
2342        }
2343        byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
2344        byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
2345        System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
2346        System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length,
2347                         a1Bytes2a.length);
2348        byte[] a1Hash = md5Digest.digest(a1Bytes2);
2349    
2350    
2351        // Next, get a hash of "AUTHENTICATE:digesturi".
2352        String a2String = ":" + digestURI;
2353        if (qop.equals("auth-int") || qop.equals("auth-conf"))
2354        {
2355          a2String += ":00000000000000000000000000000000";
2356        }
2357        byte[] a2Bytes = a2String.getBytes(charset);
2358        byte[] a2Hash  = md5Digest.digest(a2Bytes);
2359    
2360    
2361        // Get hex string representations of the last two hashes.
2362        String a1HashHex = getHexString(a1Hash);
2363        String a2HashHex = getHexString(a2Hash);
2364    
2365    
2366        // Put together the final string to hash, consisting of
2367        // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
2368        StringBuilder kdStr = new StringBuilder();
2369        kdStr.append(a1HashHex);
2370        kdStr.append(':');
2371        kdStr.append(nonce);
2372        kdStr.append(':');
2373        kdStr.append(nonceCount);
2374        kdStr.append(':');
2375        kdStr.append(cnonce);
2376        kdStr.append(':');
2377        kdStr.append(qop);
2378        kdStr.append(':');
2379        kdStr.append(a2HashHex);
2380        return md5Digest.digest(kdStr.toString().getBytes(charset));
2381      }
2382    
2383    
2384    
2385      /**
2386       * Retrieves a hexadecimal string representation of the contents of the
2387       * provided byte array.
2388       *
2389       * @param  byteArray  The byte array for which to obtain the hexadecimal
2390       *                    string representation.
2391       *
2392       * @return  The hexadecimal string representation of the contents of the
2393       *          provided byte array.
2394       */
2395      private String getHexString(byte[] byteArray)
2396      {
2397        StringBuilder buffer = new StringBuilder(2*byteArray.length);
2398        for (byte b : byteArray)
2399        {
2400          buffer.append(byteToLowerHex(b));
2401        }
2402    
2403        return buffer.toString();
2404      }
2405    
2406    
2407    
2408      /**
2409       * Retrieves the set of properties that a client may provide when performing a
2410       * SASL DIGEST-MD5 bind, mapped from the property names to their corresponding
2411       * descriptions.
2412       *
2413       * @return  The set of properties that a client may provide when performing a
2414       *          SASL DIGEST-MD5 bind, mapped from the property names to their
2415       *          corresponding descriptions.
2416       */
2417      public static LinkedHashMap<String,Message> getSASLDigestMD5Properties()
2418      {
2419        LinkedHashMap<String,Message> properties =
2420             new LinkedHashMap<String,Message>(5);
2421    
2422        properties.put(SASL_PROPERTY_AUTHID,
2423                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
2424        properties.put(SASL_PROPERTY_REALM,
2425                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
2426        properties.put(SASL_PROPERTY_QOP,
2427                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_QOP.get());
2428        properties.put(SASL_PROPERTY_DIGEST_URI,
2429                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_DIGEST_URI.get());
2430        properties.put(SASL_PROPERTY_AUTHZID,
2431                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
2432    
2433        return properties;
2434      }
2435    
2436    
2437    
2438      /**
2439       * Processes a SASL EXTERNAL bind with the provided information.
2440       *
2441       * @param  bindDN            The DN to use to bind to the Directory Server, or
2442       *                           <CODE>null</CODE> if the authentication identity
2443       *                           is to be set through some other means.
2444       * @param  saslProperties    A set of additional properties that may be needed
2445       *                           to process the SASL bind.  SASL EXTERNAL does not
2446       *                           take any properties, so this should be empty or
2447       *                           <CODE>null</CODE>.
2448       * @param  requestControls   The set of controls to include the request to the
2449       *                           server.
2450       * @param  responseControls  A list to hold the set of controls included in
2451       *                           the response from the server.
2452       *
2453       * @return  A message providing additional information about the bind if
2454       *          appropriate, or <CODE>null</CODE> if there is no special
2455       *          information available.
2456       *
2457       * @throws  ClientException  If a client-side problem prevents the bind
2458       *                           attempt from succeeding.
2459       *
2460       * @throws  LDAPException  If the bind fails or some other server-side problem
2461       *                         occurs during processing.
2462       */
2463      public String doSASLExternal(ASN1OctetString bindDN,
2464                         Map<String,List<String>> saslProperties,
2465                         ArrayList<LDAPControl> requestControls,
2466                         ArrayList<LDAPControl> responseControls)
2467             throws ClientException, LDAPException
2468      {
2469        // Make sure that no SASL properties were provided.
2470        if ((saslProperties != null) && (! saslProperties.isEmpty()))
2471        {
2472          Message message =
2473              ERR_LDAPAUTH_NO_ALLOWED_SASL_PROPERTIES.get(SASL_MECHANISM_EXTERNAL);
2474          throw new ClientException(
2475                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
2476        }
2477    
2478    
2479        // Construct the bind request and send it to the server.
2480        BindRequestProtocolOp bindRequest =
2481             new BindRequestProtocolOp(bindDN, SASL_MECHANISM_EXTERNAL, null);
2482        LDAPMessage requestMessage =
2483             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
2484                             requestControls);
2485    
2486        try
2487        {
2488          writer.writeMessage(requestMessage);
2489        }
2490        catch (IOException ioe)
2491        {
2492          Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
2493              SASL_MECHANISM_EXTERNAL, getExceptionMessage(ioe));
2494          throw new ClientException(
2495                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
2496        }
2497        catch (Exception e)
2498        {
2499          Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
2500              SASL_MECHANISM_EXTERNAL, getExceptionMessage(e));
2501          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
2502                                    message, e);
2503        }
2504    
2505    
2506        // Read the response from the server.
2507        LDAPMessage responseMessage;
2508        try
2509        {
2510          responseMessage = reader.readMessage();
2511          if (responseMessage == null)
2512          {
2513            Message message =
2514                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
2515            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
2516                                      message);
2517          }
2518        }
2519        catch (IOException ioe)
2520        {
2521          Message message =
2522              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
2523          throw new ClientException(
2524                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
2525        }
2526        catch (ASN1Exception ae)
2527        {
2528          Message message =
2529              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
2530          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
2531                                    message, ae);
2532        }
2533        catch (LDAPException le)
2534        {
2535          Message message =
2536              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
2537          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
2538                                    message, le);
2539        }
2540        catch (Exception e)
2541        {
2542          Message message =
2543              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
2544          throw new ClientException(
2545                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2546        }
2547    
2548    
2549        // See if there are any controls in the response.  If so, then add them to
2550        // the response controls list.
2551        ArrayList<LDAPControl> respControls = responseMessage.getControls();
2552        if ((respControls != null) && (! respControls.isEmpty()))
2553        {
2554          responseControls.addAll(respControls);
2555        }
2556    
2557    
2558        // Look at the protocol op from the response.  If it's a bind response, then
2559        // continue.  If it's an extended response, then it could be a notice of
2560        // disconnection so check for that.  Otherwise, generate an error.
2561        switch (responseMessage.getProtocolOpType())
2562        {
2563          case OP_TYPE_BIND_RESPONSE:
2564            // We'll deal with this later.
2565            break;
2566    
2567          case OP_TYPE_EXTENDED_RESPONSE:
2568            ExtendedResponseProtocolOp extendedResponse =
2569                 responseMessage.getExtendedResponseProtocolOp();
2570            String responseOID = extendedResponse.getOID();
2571            if ((responseOID != null) &&
2572                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
2573            {
2574              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
2575                  get(extendedResponse.getResultCode(),
2576                      extendedResponse.getErrorMessage());
2577              throw new LDAPException(extendedResponse.getResultCode(), message);
2578            }
2579            else
2580            {
2581              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
2582                  String.valueOf(extendedResponse));
2583              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
2584                                        message);
2585            }
2586    
2587          default:
2588            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
2589                String.valueOf(responseMessage.getProtocolOp()));
2590            throw new ClientException(
2591                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
2592        }
2593    
2594    
2595        BindResponseProtocolOp bindResponse =
2596             responseMessage.getBindResponseProtocolOp();
2597        int resultCode = bindResponse.getResultCode();
2598        if (resultCode == LDAPResultCode.SUCCESS)
2599        {
2600          // FIXME -- Need to look for things like password expiration warning,
2601          // reset notice, etc.
2602          return null;
2603        }
2604    
2605        // FIXME -- Add support for referrals.
2606    
2607        Message message =
2608            ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_EXTERNAL);
2609        throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
2610                                message, bindResponse.getMatchedDN(), null);
2611      }
2612    
2613    
2614    
2615      /**
2616       * Retrieves the set of properties that a client may provide when performing a
2617       * SASL EXTERNAL bind, mapped from the property names to their corresponding
2618       * descriptions.
2619       *
2620       * @return  The set of properties that a client may provide when performing a
2621       *          SASL EXTERNAL bind, mapped from the property names to their
2622       *          corresponding descriptions.
2623       */
2624      public static LinkedHashMap<String,Message> getSASLExternalProperties()
2625      {
2626        // There are no properties for the SASL EXTERNAL mechanism.
2627        return new LinkedHashMap<String,Message>(0);
2628      }
2629    
2630    
2631    
2632      /**
2633       * Processes a SASL GSSAPI bind with the provided information.
2634       *
2635       * @param  bindDN            The DN to use to bind to the Directory Server, or
2636       *                           <CODE>null</CODE> if the authentication identity
2637       *                           is to be set through some other means.
2638       * @param  bindPassword      The password to use to bind to the Directory
2639       *                           Server.
2640       * @param  saslProperties    A set of additional properties that may be needed
2641       *                           to process the SASL bind.  SASL EXTERNAL does not
2642       *                           take any properties, so this should be empty or
2643       *                           <CODE>null</CODE>.
2644       * @param  requestControls   The set of controls to include the request to the
2645       *                           server.
2646       * @param  responseControls  A list to hold the set of controls included in
2647       *                           the response from the server.
2648       *
2649       * @return  A message providing additional information about the bind if
2650       *          appropriate, or <CODE>null</CODE> if there is no special
2651       *          information available.
2652       *
2653       * @throws  ClientException  If a client-side problem prevents the bind
2654       *                           attempt from succeeding.
2655       *
2656       * @throws  LDAPException  If the bind fails or some other server-side problem
2657       *                         occurs during processing.
2658       */
2659      public String doSASLGSSAPI(ASN1OctetString bindDN,
2660                         ASN1OctetString bindPassword,
2661                         Map<String,List<String>> saslProperties,
2662                         ArrayList<LDAPControl> requestControls,
2663                         ArrayList<LDAPControl> responseControls)
2664             throws ClientException, LDAPException
2665      {
2666        String kdc     = null;
2667        String realm   = null;
2668    
2669        gssapiBindDN  = bindDN;
2670        gssapiAuthID  = null;
2671        gssapiAuthzID = null;
2672        gssapiQoP     = "auth";
2673    
2674        if (bindPassword == null)
2675        {
2676          gssapiAuthPW = null;
2677        }
2678        else
2679        {
2680          gssapiAuthPW = bindPassword.stringValue().toCharArray();
2681        }
2682    
2683    
2684        // Evaluate the properties provided.  The authID is required.  The authzID,
2685        // KDC, QoP, and realm are optional.
2686        if ((saslProperties == null) || saslProperties.isEmpty())
2687        {
2688          Message message =
2689              ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_GSSAPI);
2690          throw new ClientException(
2691                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
2692        }
2693    
2694        Iterator<String> propertyNames = saslProperties.keySet().iterator();
2695        while (propertyNames.hasNext())
2696        {
2697          String name      = propertyNames.next();
2698          String lowerName = toLowerCase(name);
2699    
2700          if (lowerName.equals(SASL_PROPERTY_AUTHID))
2701          {
2702            List<String> values = saslProperties.get(name);
2703            Iterator<String> iterator = values.iterator();
2704            if (iterator.hasNext())
2705            {
2706              gssapiAuthID = iterator.next();
2707    
2708              if (iterator.hasNext())
2709              {
2710                Message message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
2711                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
2712                                          message);
2713              }
2714            }
2715          }
2716          else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
2717          {
2718            List<String> values = saslProperties.get(name);
2719            Iterator<String> iterator = values.iterator();
2720            if (iterator.hasNext())
2721            {
2722              gssapiAuthzID = iterator.next();
2723    
2724              if (iterator.hasNext())
2725              {
2726                Message message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
2727                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
2728                                          message);
2729              }
2730            }
2731          }
2732          else if (lowerName.equals(SASL_PROPERTY_KDC))
2733          {
2734            List<String> values = saslProperties.get(name);
2735            Iterator<String> iterator = values.iterator();
2736            if (iterator.hasNext())
2737            {
2738              kdc = iterator.next();
2739    
2740              if (iterator.hasNext())
2741              {
2742                Message message = ERR_LDAPAUTH_KDC_SINGLE_VALUED.get();
2743                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
2744                                          message);
2745              }
2746            }
2747          }
2748          else if (lowerName.equals(SASL_PROPERTY_QOP))
2749          {
2750            List<String> values = saslProperties.get(name);
2751            Iterator<String> iterator = values.iterator();
2752            if (iterator.hasNext())
2753            {
2754              gssapiQoP = toLowerCase(iterator.next());
2755    
2756              if (iterator.hasNext())
2757              {
2758                Message message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
2759                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
2760                                          message);
2761              }
2762    
2763              if (gssapiQoP.equals("auth"))
2764              {
2765                // This is always fine.
2766              }
2767              else if (gssapiQoP.equals("auth-int") ||
2768                       gssapiQoP.equals("auth-conf"))
2769              {
2770                // FIXME -- Add support for integrity and confidentiality.
2771                Message message =
2772                    ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(gssapiQoP);
2773                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
2774                                          message);
2775              }
2776              else
2777              {
2778                // This is an illegal value.
2779                Message message = ERR_LDAPAUTH_GSSAPI_INVALID_QOP.get(gssapiQoP);
2780                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
2781                                          message);
2782              }
2783            }
2784          }
2785          else if (lowerName.equals(SASL_PROPERTY_REALM))
2786          {
2787            List<String> values = saslProperties.get(name);
2788            Iterator<String> iterator = values.iterator();
2789            if (iterator.hasNext())
2790            {
2791              realm = iterator.next();
2792    
2793              if (iterator.hasNext())
2794              {
2795                Message message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get();
2796                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
2797                                          message);
2798              }
2799            }
2800          }
2801          else
2802          {
2803            Message message =
2804                ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_GSSAPI);
2805            throw new ClientException(
2806                    LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
2807          }
2808        }
2809    
2810    
2811        // Make sure that the authID was provided.
2812        if ((gssapiAuthID == null) || (gssapiAuthID.length() == 0))
2813        {
2814          Message message =
2815              ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_GSSAPI);
2816          throw new ClientException(
2817                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
2818        }
2819    
2820    
2821        // See if an authzID was provided.  If not, then use the authID.
2822        if (gssapiAuthzID == null)
2823        {
2824          gssapiAuthzID = gssapiAuthID;
2825        }
2826    
2827    
2828        // See if the realm and/or KDC were specified.  If so, then set properties
2829        // that will allow them to be used.  Otherwise, we'll hope that the
2830        // underlying system has a valid Kerberos client configuration.
2831        if (realm != null)
2832        {
2833          System.setProperty(KRBV_PROPERTY_REALM, realm);
2834        }
2835    
2836        if (kdc != null)
2837        {
2838          System.setProperty(KRBV_PROPERTY_KDC, kdc);
2839        }
2840    
2841    
2842        // Since we're going to be using JAAS behind the scenes, we need to have a
2843        // JAAS configuration.  Rather than always requiring the user to provide it,
2844        // we'll write one to a temporary file that will be deleted when the JVM
2845        // exits.
2846        String configFileName;
2847        try
2848        {
2849          File tempFile = File.createTempFile("login", "conf");
2850          configFileName = tempFile.getAbsolutePath();
2851          tempFile.deleteOnExit();
2852          BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false));
2853    
2854          w.write(getClass().getName() + " {");
2855          w.newLine();
2856    
2857          w.write("  com.sun.security.auth.module.Krb5LoginModule required " +
2858                  "client=TRUE useTicketCache=TRUE;");
2859          w.newLine();
2860    
2861          w.write("};");
2862          w.newLine();
2863    
2864          w.flush();
2865          w.close();
2866        }
2867        catch (Exception e)
2868        {
2869          Message message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get(
2870              getExceptionMessage(e));
2871          throw new ClientException(
2872                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2873        }
2874    
2875        System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName);
2876        System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "true");
2877    
2878    
2879        // The rest of this code must be executed via JAAS, so it will have to go
2880        // in the "run" method.
2881        LoginContext loginContext;
2882        try
2883        {
2884          loginContext = new LoginContext(getClass().getName(), this);
2885          loginContext.login();
2886        }
2887        catch (Exception e)
2888        {
2889          Message message = ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED.get(
2890              getExceptionMessage(e));
2891          throw new ClientException(
2892                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2893        }
2894    
2895        try
2896        {
2897          Subject.doAs(loginContext.getSubject(), this);
2898        }
2899        catch (Exception e)
2900        {
2901          if (e instanceof ClientException)
2902          {
2903            throw (ClientException) e;
2904          }
2905          else if (e instanceof LDAPException)
2906          {
2907            throw (LDAPException) e;
2908          }
2909    
2910          Message message = ERR_LDAPAUTH_GSSAPI_REMOTE_AUTHENTICATION_FAILED.get(
2911                  getExceptionMessage(e));
2912          throw new ClientException(
2913                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2914        }
2915    
2916    
2917        // FIXME --  Need to make sure we handle request and response controls
2918        // properly, and also check for any possible message to send back to the
2919        // client.
2920        return null;
2921      }
2922    
2923    
2924    
2925      /**
2926       * Retrieves the set of properties that a client may provide when performing a
2927       * SASL EXTERNAL bind, mapped from the property names to their corresponding
2928       * descriptions.
2929       *
2930       * @return  The set of properties that a client may provide when performing a
2931       *          SASL EXTERNAL bind, mapped from the property names to their
2932       *          corresponding descriptions.
2933       */
2934      public static LinkedHashMap<String,Message> getSASLGSSAPIProperties()
2935      {
2936        LinkedHashMap<String,Message> properties =
2937             new LinkedHashMap<String,Message>(4);
2938    
2939        properties.put(SASL_PROPERTY_AUTHID,
2940                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
2941        properties.put(SASL_PROPERTY_AUTHZID,
2942                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
2943        properties.put(SASL_PROPERTY_KDC,
2944                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_KDC.get());
2945        properties.put(SASL_PROPERTY_REALM,
2946                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
2947    
2948        return properties;
2949      }
2950    
2951    
2952    
2953      /**
2954       * Processes a SASL PLAIN bind with the provided information.
2955       *
2956       * @param  bindDN            The DN to use to bind to the Directory Server, or
2957       *                           <CODE>null</CODE> if the authentication identity
2958       *                           is to be set through some other means.
2959       * @param  bindPassword      The password to use to bind to the Directory
2960       *                           Server.
2961       * @param  saslProperties    A set of additional properties that may be needed
2962       *                           to process the SASL bind.
2963       * @param  requestControls   The set of controls to include the request to the
2964       *                           server.
2965       * @param  responseControls  A list to hold the set of controls included in
2966       *                           the response from the server.
2967       *
2968       * @return  A message providing additional information about the bind if
2969       *          appropriate, or <CODE>null</CODE> if there is no special
2970       *          information available.
2971       *
2972       * @throws  ClientException  If a client-side problem prevents the bind
2973       *                           attempt from succeeding.
2974       *
2975       * @throws  LDAPException  If the bind fails or some other server-side problem
2976       *                         occurs during processing.
2977       */
2978      public String doSASLPlain(ASN1OctetString bindDN,
2979                         ASN1OctetString bindPassword,
2980                         Map<String,List<String>> saslProperties,
2981                         ArrayList<LDAPControl> requestControls,
2982                         ArrayList<LDAPControl> responseControls)
2983             throws ClientException, LDAPException
2984      {
2985        String authID  = null;
2986        String authzID = null;
2987    
2988    
2989        // Evaluate the properties provided.  The authID is required, and authzID is
2990        // optional.
2991        if ((saslProperties == null) || saslProperties.isEmpty())
2992        {
2993          Message message =
2994              ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_PLAIN);
2995          throw new ClientException(
2996                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
2997        }
2998    
2999        Iterator<String> propertyNames = saslProperties.keySet().iterator();
3000        while (propertyNames.hasNext())
3001        {
3002          String name      = propertyNames.next();
3003          String lowerName = toLowerCase(name);
3004    
3005          if (lowerName.equals(SASL_PROPERTY_AUTHID))
3006          {
3007            List<String> values = saslProperties.get(name);
3008            Iterator<String> iterator = values.iterator();
3009            if (iterator.hasNext())
3010            {
3011              authID = iterator.next();
3012    
3013              if (iterator.hasNext())
3014              {
3015                Message message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
3016                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
3017                                          message);
3018              }
3019            }
3020          }
3021          else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
3022          {
3023            List<String> values = saslProperties.get(name);
3024            Iterator<String> iterator = values.iterator();
3025            if (iterator.hasNext())
3026            {
3027              authzID = iterator.next();
3028    
3029              if (iterator.hasNext())
3030              {
3031                Message message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
3032                throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
3033                                          message);
3034              }
3035            }
3036          }
3037          else
3038          {
3039            Message message =
3040                ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_PLAIN);
3041            throw new ClientException(
3042                    LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
3043          }
3044        }
3045    
3046    
3047        // Make sure that at least the authID was provided.
3048        if ((authID == null) || (authID.length() == 0))
3049        {
3050          Message message =
3051              ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_PLAIN);
3052          throw new ClientException(
3053                  LDAPResultCode.CLIENT_SIDE_PARAM_ERROR, message);
3054        }
3055    
3056    
3057        // See if the password was null.  If so, then interactively prompt it from
3058        // the user.
3059        if (bindPassword == null)
3060        {
3061          System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(authID));
3062          char[] pwChars = PasswordReader.readPassword();
3063          if (pwChars == null)
3064          {
3065            bindPassword = new ASN1OctetString();
3066          }
3067          else
3068          {
3069            bindPassword = new ASN1OctetString(getBytes(pwChars));
3070            Arrays.fill(pwChars, '\u0000');
3071          }
3072        }
3073    
3074    
3075        // Construct the bind request and send it to the server.
3076        StringBuilder credBuffer = new StringBuilder();
3077        if (authzID != null)
3078        {
3079          credBuffer.append(authzID);
3080        }
3081        credBuffer.append('\u0000');
3082        credBuffer.append(authID);
3083        credBuffer.append('\u0000');
3084        credBuffer.append(bindPassword.stringValue());
3085    
3086        ASN1OctetString saslCredentials =
3087             new ASN1OctetString(credBuffer.toString());
3088        BindRequestProtocolOp bindRequest =
3089             new BindRequestProtocolOp(bindDN, SASL_MECHANISM_PLAIN,
3090                                    saslCredentials);
3091        LDAPMessage requestMessage =
3092             new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
3093                             requestControls);
3094    
3095        try
3096        {
3097          writer.writeMessage(requestMessage);
3098        }
3099        catch (IOException ioe)
3100        {
3101          Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3102              SASL_MECHANISM_PLAIN, getExceptionMessage(ioe));
3103          throw new ClientException(
3104                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3105        }
3106        catch (Exception e)
3107        {
3108          Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3109              SASL_MECHANISM_PLAIN, getExceptionMessage(e));
3110          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
3111                                    message, e);
3112        }
3113    
3114    
3115        // Read the response from the server.
3116        LDAPMessage responseMessage;
3117        try
3118        {
3119          responseMessage = reader.readMessage();
3120          if (responseMessage == null)
3121          {
3122            Message message =
3123                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
3124            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
3125                                      message);
3126          }
3127        }
3128        catch (IOException ioe)
3129        {
3130          Message message =
3131              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
3132          throw new ClientException(
3133                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3134        }
3135        catch (ASN1Exception ae)
3136        {
3137          Message message =
3138              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
3139          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3140                                    message, ae);
3141        }
3142        catch (LDAPException le)
3143        {
3144          Message message =
3145              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
3146          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3147                                    message, le);
3148        }
3149        catch (Exception e)
3150        {
3151          Message message =
3152              ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
3153          throw new ClientException(
3154                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
3155        }
3156    
3157    
3158        // See if there are any controls in the response.  If so, then add them to
3159        // the response controls list.
3160        ArrayList<LDAPControl> respControls = responseMessage.getControls();
3161        if ((respControls != null) && (! respControls.isEmpty()))
3162        {
3163          responseControls.addAll(respControls);
3164        }
3165    
3166    
3167        // Look at the protocol op from the response.  If it's a bind response, then
3168        // continue.  If it's an extended response, then it could be a notice of
3169        // disconnection so check for that.  Otherwise, generate an error.
3170        switch (responseMessage.getProtocolOpType())
3171        {
3172          case OP_TYPE_BIND_RESPONSE:
3173            // We'll deal with this later.
3174            break;
3175    
3176          case OP_TYPE_EXTENDED_RESPONSE:
3177            ExtendedResponseProtocolOp extendedResponse =
3178                 responseMessage.getExtendedResponseProtocolOp();
3179            String responseOID = extendedResponse.getOID();
3180            if ((responseOID != null) &&
3181                responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
3182            {
3183              Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
3184                  get(extendedResponse.getResultCode(),
3185                      extendedResponse.getErrorMessage());
3186              throw new LDAPException(extendedResponse.getResultCode(), message);
3187            }
3188            else
3189            {
3190              Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
3191                  String.valueOf(extendedResponse));
3192              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3193                                        message);
3194            }
3195    
3196          default:
3197            Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
3198                String.valueOf(responseMessage.getProtocolOp()));
3199            throw new ClientException(
3200                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
3201        }
3202    
3203    
3204        BindResponseProtocolOp bindResponse =
3205             responseMessage.getBindResponseProtocolOp();
3206        int resultCode = bindResponse.getResultCode();
3207        if (resultCode == LDAPResultCode.SUCCESS)
3208        {
3209          // FIXME -- Need to look for things like password expiration warning,
3210          // reset notice, etc.
3211          return null;
3212        }
3213    
3214        // FIXME -- Add support for referrals.
3215    
3216        Message message = ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_PLAIN);
3217        throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
3218                                message, bindResponse.getMatchedDN(), null);
3219      }
3220    
3221    
3222    
3223      /**
3224       * Retrieves the set of properties that a client may provide when performing a
3225       * SASL PLAIN bind, mapped from the property names to their corresponding
3226       * descriptions.
3227       *
3228       * @return  The set of properties that a client may provide when performing a
3229       *          SASL PLAIN bind, mapped from the property names to their
3230       *          corresponding descriptions.
3231       */
3232      public static LinkedHashMap<String,Message> getSASLPlainProperties()
3233      {
3234        LinkedHashMap<String,Message> properties =
3235             new LinkedHashMap<String,Message>(2);
3236    
3237        properties.put(SASL_PROPERTY_AUTHID,
3238                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
3239        properties.put(SASL_PROPERTY_AUTHZID,
3240                       INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
3241    
3242        return properties;
3243      }
3244    
3245    
3246    
3247      /**
3248       * Performs a privileged operation under JAAS so that the local authentication
3249       * information can be available for the SASL bind to the Directory Server.
3250       *
3251       * @return  A placeholder object in order to comply with the
3252       *          <CODE>PrivilegedExceptionAction</CODE> interface.
3253       *
3254       * @throws  ClientException  If a client-side problem occurs during the bind
3255       *                           processing.
3256       *
3257       * @throws  LDAPException  If a server-side problem occurs during the bind
3258       *                         processing.
3259       */
3260      public Object run()
3261             throws ClientException, LDAPException
3262      {
3263        if (saslMechanism == null)
3264        {
3265          Message message = ERR_LDAPAUTH_NONSASL_RUN_INVOCATION.get(getBacktrace());
3266          throw new ClientException(
3267                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
3268        }
3269        else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
3270        {
3271          // Create the property map that will be used by the internal SASL handler.
3272          HashMap<String,String> saslProperties = new HashMap<String,String>();
3273          saslProperties.put(Sasl.QOP, gssapiQoP);
3274          saslProperties.put(Sasl.SERVER_AUTH, "true");
3275    
3276    
3277          // Create the SASL client that we will use to actually perform the
3278          // authentication.
3279          SaslClient saslClient;
3280          try
3281          {
3282            saslClient =
3283                 Sasl.createSaslClient(new String[] { SASL_MECHANISM_GSSAPI },
3284                                       gssapiAuthzID, "ldap", hostName,
3285                                       saslProperties, this);
3286          }
3287          catch (Exception e)
3288          {
3289            Message message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get(
3290                getExceptionMessage(e));
3291            throw new ClientException(
3292                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
3293          }
3294    
3295    
3296          // Get the SASL credentials to include in the initial bind request.
3297          ASN1OctetString saslCredentials;
3298          if (saslClient.hasInitialResponse())
3299          {
3300            try
3301            {
3302              byte[] credBytes = saslClient.evaluateChallenge(new byte[0]);
3303              saslCredentials = new ASN1OctetString(credBytes);
3304            }
3305            catch (Exception e)
3306            {
3307              Message message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_INITIAL_CHALLENGE.
3308                  get(getExceptionMessage(e));
3309              throw new ClientException(
3310                      LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3311                                        message, e);
3312            }
3313          }
3314          else
3315          {
3316            saslCredentials = null;
3317          }
3318    
3319    
3320          BindRequestProtocolOp bindRequest =
3321               new BindRequestProtocolOp(gssapiBindDN, SASL_MECHANISM_GSSAPI,
3322                                         saslCredentials);
3323          // FIXME -- Add controls here?
3324          LDAPMessage requestMessage =
3325               new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
3326    
3327          try
3328          {
3329            writer.writeMessage(requestMessage);
3330          }
3331          catch (IOException ioe)
3332          {
3333            Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3334                SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe));
3335            throw new ClientException(
3336                    LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3337          }
3338          catch (Exception e)
3339          {
3340            Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3341                SASL_MECHANISM_GSSAPI, getExceptionMessage(e));
3342            throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
3343                                      message, e);
3344          }
3345    
3346    
3347          // Read the response from the server.
3348          LDAPMessage responseMessage;
3349          try
3350          {
3351            responseMessage = reader.readMessage();
3352            if (responseMessage == null)
3353            {
3354              Message message =
3355                  ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
3356              throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
3357                                        message);
3358            }
3359          }
3360          catch (IOException ioe)
3361          {
3362            Message message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3363                getExceptionMessage(ioe));
3364            throw new ClientException(
3365                    LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3366          }
3367          catch (ASN1Exception ae)
3368          {
3369            Message message =
3370                ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
3371            throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3372                                      message, ae);
3373          }
3374          catch (LDAPException le)
3375          {
3376            Message message =
3377                ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
3378            throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3379                                      message, le);
3380          }
3381          catch (Exception e)
3382          {
3383            Message message =
3384                ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
3385            throw new ClientException(
3386                    LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
3387          }
3388    
3389    
3390          // FIXME -- Handle response controls.
3391    
3392    
3393          // Look at the protocol op from the response.  If it's a bind response,
3394          // then continue.  If it's an extended response, then it could be a notice
3395          // of disconnection so check for that.  Otherwise, generate an error.
3396          switch (responseMessage.getProtocolOpType())
3397          {
3398            case OP_TYPE_BIND_RESPONSE:
3399              // We'll deal with this later.
3400              break;
3401    
3402            case OP_TYPE_EXTENDED_RESPONSE:
3403              ExtendedResponseProtocolOp extendedResponse =
3404                   responseMessage.getExtendedResponseProtocolOp();
3405              String responseOID = extendedResponse.getOID();
3406              if ((responseOID != null) &&
3407                  responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
3408              {
3409                Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
3410                    get(extendedResponse.getResultCode(),
3411                        extendedResponse.getErrorMessage());
3412                throw new LDAPException(extendedResponse.getResultCode(), message);
3413              }
3414              else
3415              {
3416                Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
3417                    String.valueOf(extendedResponse));
3418                throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3419                                          message);
3420              }
3421    
3422            default:
3423              Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
3424                  String.valueOf(responseMessage.getProtocolOp()));
3425              throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3426                                        message);
3427          }
3428    
3429    
3430          while (true)
3431          {
3432            BindResponseProtocolOp bindResponse =
3433                 responseMessage.getBindResponseProtocolOp();
3434            int resultCode = bindResponse.getResultCode();
3435            if (resultCode == LDAPResultCode.SUCCESS)
3436            {
3437              // We should be done after this, but we still need to look for and
3438              // handle the server SASL credentials.
3439              ASN1OctetString serverSASLCredentials =
3440                   bindResponse.getServerSASLCredentials();
3441              if (serverSASLCredentials != null)
3442              {
3443                try
3444                {
3445                  saslClient.evaluateChallenge(serverSASLCredentials.value());
3446                }
3447                catch (Exception e)
3448                {
3449                  Message message =
3450                      ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.
3451                        get(getExceptionMessage(e));
3452                  throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3453                                            message, e);
3454                }
3455              }
3456    
3457    
3458              // Just to be sure, check that the login really is complete.
3459              if (! saslClient.isComplete())
3460              {
3461                Message message =
3462                    ERR_LDAPAUTH_GSSAPI_UNEXPECTED_SUCCESS_RESPONSE.get();
3463                throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3464                                          message);
3465              }
3466    
3467              break;
3468            }
3469            else if (resultCode == LDAPResultCode.SASL_BIND_IN_PROGRESS)
3470            {
3471              // Read the response and process the server SASL credentials.
3472              ASN1OctetString serverSASLCredentials =
3473                   bindResponse.getServerSASLCredentials();
3474              byte[] credBytes;
3475              try
3476              {
3477                if (serverSASLCredentials == null)
3478                {
3479                  credBytes = saslClient.evaluateChallenge(new byte[0]);
3480                }
3481                else
3482                {
3483                  credBytes =
3484                       saslClient.evaluateChallenge(serverSASLCredentials.value());
3485                }
3486              }
3487              catch (Exception e)
3488              {
3489                Message message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.
3490                    get(getExceptionMessage(e));
3491                throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3492                                          message, e);
3493              }
3494    
3495    
3496              // Send the next bind in the sequence to the server.
3497              bindRequest =
3498                   new BindRequestProtocolOp(gssapiBindDN, SASL_MECHANISM_GSSAPI,
3499                                             new ASN1OctetString(credBytes));
3500              // FIXME -- Add controls here?
3501              requestMessage =
3502                   new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
3503    
3504    
3505              try
3506              {
3507                writer.writeMessage(requestMessage);
3508              }
3509              catch (IOException ioe)
3510              {
3511                Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3512                    SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe));
3513                throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
3514                                          message, ioe);
3515              }
3516              catch (Exception e)
3517              {
3518                Message message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3519                    SASL_MECHANISM_GSSAPI, getExceptionMessage(e));
3520                throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
3521                                          message, e);
3522              }
3523    
3524    
3525              // Read the response from the server.
3526              try
3527              {
3528                responseMessage = reader.readMessage();
3529                if (responseMessage == null)
3530                {
3531                  Message message =
3532                      ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
3533                  throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
3534                                            message);
3535                }
3536              }
3537              catch (IOException ioe)
3538              {
3539                Message message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3540                    getExceptionMessage(ioe));
3541                throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
3542                                          message, ioe);
3543              }
3544              catch (ASN1Exception ae)
3545              {
3546                Message message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3547                    getExceptionMessage(ae));
3548                throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3549                                          message, ae);
3550              }
3551              catch (LDAPException le)
3552              {
3553                Message message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3554                    getExceptionMessage(le));
3555                throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3556                                          message, le);
3557              }
3558              catch (Exception e)
3559              {
3560                Message message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3561                    getExceptionMessage(e));
3562                throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3563                                          message, e);
3564              }
3565    
3566    
3567              // FIXME -- Handle response controls.
3568    
3569    
3570              // Look at the protocol op from the response.  If it's a bind
3571              // response, then continue.  If it's an extended response, then it
3572              // could be a notice of disconnection so check for that.  Otherwise,
3573              // generate an error.
3574              switch (responseMessage.getProtocolOpType())
3575              {
3576                case OP_TYPE_BIND_RESPONSE:
3577                  // We'll deal with this later.
3578                  break;
3579    
3580                case OP_TYPE_EXTENDED_RESPONSE:
3581                  ExtendedResponseProtocolOp extendedResponse =
3582                       responseMessage.getExtendedResponseProtocolOp();
3583                  String responseOID = extendedResponse.getOID();
3584                  if ((responseOID != null) &&
3585                      responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
3586                  {
3587                    Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
3588                        get(extendedResponse.getResultCode(),
3589                            extendedResponse.getErrorMessage());
3590                    throw new LDAPException(extendedResponse.getResultCode(),
3591                            message);
3592                  }
3593                  else
3594                  {
3595                    Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
3596                        String.valueOf(extendedResponse));
3597                    throw new ClientException(
3598                                   LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
3599                  }
3600    
3601                default:
3602                  Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
3603                      String.valueOf(responseMessage.getProtocolOp()));
3604                  throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
3605                                            message);
3606              }
3607            }
3608            else
3609            {
3610              // This is an error.
3611              Message message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get();
3612              throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
3613                                      message, bindResponse.getMatchedDN(),
3614                                      null);
3615            }
3616          }
3617        }
3618        else
3619        {
3620          Message message = ERR_LDAPAUTH_UNEXPECTED_RUN_INVOCATION.get(
3621              saslMechanism, getBacktrace());
3622          throw new ClientException(
3623                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
3624        }
3625    
3626    
3627        // FIXME -- Need to look for things like password expiration warning, reset
3628        // notice, etc.
3629        return null;
3630      }
3631    
3632    
3633    
3634      /**
3635       * Handles the authentication callbacks to provide information needed by the
3636       * JAAS login process.
3637       *
3638       * @param  callbacks  The callbacks needed to provide information for the JAAS
3639       *                    login process.
3640       *
3641       * @throws  UnsupportedCallbackException  If an unexpected callback is
3642       *                                        included in the provided set.
3643       */
3644      public void handle(Callback[] callbacks)
3645             throws UnsupportedCallbackException
3646      {
3647        if (saslMechanism ==  null)
3648        {
3649          Message message =
3650              ERR_LDAPAUTH_NONSASL_CALLBACK_INVOCATION.get(getBacktrace());
3651          throw new UnsupportedCallbackException(callbacks[0], message.toString());
3652        }
3653        else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
3654        {
3655          for (Callback cb : callbacks)
3656          {
3657            if (cb instanceof NameCallback)
3658            {
3659              ((NameCallback) cb).setName(gssapiAuthID);
3660            }
3661            else if (cb instanceof PasswordCallback)
3662            {
3663              if (gssapiAuthPW == null)
3664              {
3665                System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(gssapiAuthID));
3666                gssapiAuthPW = PasswordReader.readPassword();
3667              }
3668    
3669              ((PasswordCallback) cb).setPassword(gssapiAuthPW);
3670            }
3671            else
3672            {
3673              Message message =
3674                  ERR_LDAPAUTH_UNEXPECTED_GSSAPI_CALLBACK.get(String.valueOf(cb));
3675              throw new UnsupportedCallbackException(cb, message.toString());
3676            }
3677          }
3678        }
3679        else
3680        {
3681          Message message = ERR_LDAPAUTH_UNEXPECTED_CALLBACK_INVOCATION.get(
3682              saslMechanism, getBacktrace());
3683          throw new UnsupportedCallbackException(callbacks[0], message.toString());
3684        }
3685      }
3686    
3687    
3688    
3689      /**
3690       * Uses the "Who Am I?" extended operation to request that the server provide
3691       * the client with the authorization identity for this connection.
3692       *
3693       * @return  An ASN.1 octet string containing the authorization identity, or
3694       *          <CODE>null</CODE> if the client is not authenticated or is
3695       *          authenticated anonymously.
3696       *
3697       * @throws  ClientException  If a client-side problem occurs during the
3698       *                           request processing.
3699       *
3700       * @throws  LDAPException  If a server-side problem occurs during the request
3701       *                         processing.
3702       */
3703      public ASN1OctetString requestAuthorizationIdentity()
3704             throws ClientException, LDAPException
3705      {
3706        // Construct the extended request and send it to the server.
3707        ExtendedRequestProtocolOp extendedRequest =
3708             new ExtendedRequestProtocolOp(OID_WHO_AM_I_REQUEST);
3709        LDAPMessage requestMessage =
3710             new LDAPMessage(nextMessageID.getAndIncrement(), extendedRequest);
3711    
3712        try
3713        {
3714          writer.writeMessage(requestMessage);
3715        }
3716        catch (IOException ioe)
3717        {
3718          Message message =
3719              ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(ioe));
3720          throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
3721                  message, ioe);
3722        }
3723        catch (Exception e)
3724        {
3725          Message message =
3726              ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(e));
3727          throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
3728                                    message, e);
3729        }
3730    
3731    
3732        // Read the response from the server.
3733        LDAPMessage responseMessage;
3734        try
3735        {
3736          responseMessage = reader.readMessage();
3737          if (responseMessage == null)
3738          {
3739            Message message =
3740                ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
3741            throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
3742                                      message);
3743          }
3744        }
3745        catch (IOException ioe)
3746        {
3747          Message message = ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(
3748              getExceptionMessage(ioe));
3749          throw new ClientException(
3750                  LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3751        }
3752        catch (ASN1Exception ae)
3753        {
3754          Message message =
3755              ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(ae));
3756          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3757                                    message, ae);
3758        }
3759        catch (LDAPException le)
3760        {
3761          Message message =
3762              ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(le));
3763          throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
3764                                    message, le);
3765        }
3766        catch (Exception e)
3767        {
3768          Message message =
3769              ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e));
3770          throw new ClientException(
3771                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
3772        }
3773    
3774    
3775        // If the protocol op isn't an extended response, then that's a problem.
3776        if (responseMessage.getProtocolOpType() != OP_TYPE_EXTENDED_RESPONSE)
3777        {
3778          Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
3779              String.valueOf(responseMessage.getProtocolOp()));
3780          throw new ClientException(
3781                  LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
3782        }
3783    
3784    
3785        // Get the extended response and see if it has the "notice of disconnection"
3786        // OID.  If so, then the server is closing the connection.
3787        ExtendedResponseProtocolOp extendedResponse =
3788             responseMessage.getExtendedResponseProtocolOp();
3789        String responseOID = extendedResponse.getOID();
3790        if ((responseOID != null) &&
3791            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
3792        {
3793          Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.get(
3794              extendedResponse.getResultCode(), extendedResponse.getErrorMessage());
3795          throw new LDAPException(extendedResponse.getResultCode(), message);
3796        }
3797    
3798    
3799        // It isn't a notice of disconnection so it must be the "Who Am I?"
3800        // response and the value would be the authorization ID.  However, first
3801        // check that it was successful.  If it was not, then fail.
3802        int resultCode = extendedResponse.getResultCode();
3803        if (resultCode != LDAPResultCode.SUCCESS)
3804        {
3805          Message message = ERR_LDAPAUTH_WHOAMI_FAILED.get();
3806          throw new LDAPException(resultCode, extendedResponse.getErrorMessage(),
3807                                  message, extendedResponse.getMatchedDN(),
3808                                  null);
3809        }
3810    
3811    
3812        // Get the authorization ID (if there is one) and return it to the caller.
3813        ASN1OctetString authzID = extendedResponse.getValue();
3814        if ((authzID == null) || (authzID.value() == null) ||
3815            (authzID.value().length == 0))
3816        {
3817          return null;
3818        }
3819    
3820        String valueString = authzID.stringValue();
3821        if ((valueString == null) || (valueString.length() == 0) ||
3822            valueString.equalsIgnoreCase("dn:"))
3823        {
3824          return null;
3825        }
3826    
3827        return authzID;
3828      }
3829    }
3830