From 8a180ad417c26429cd3774c0046165c40ad1010a Mon Sep 17 00:00:00 2001
From: Jean-Noël Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Thu, 28 Apr 2016 09:04:35 +0000
Subject: [PATCH] LDAPAuthenticationHandler.java: Remove code duplication

---
 opendj-server-legacy/src/main/java/org/opends/server/tools/LDAPAuthenticationHandler.java | 1964 ++++++++++++++---------------------------------------------
 1 files changed, 469 insertions(+), 1,495 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/LDAPAuthenticationHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/LDAPAuthenticationHandler.java
index c366e7c..1657009 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/LDAPAuthenticationHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/LDAPAuthenticationHandler.java
@@ -16,6 +16,13 @@
  */
 package org.opends.server.tools;
 
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+
+import static org.opends.messages.ToolMessages.*;
+import static org.opends.server.protocols.ldap.LDAPConstants.*;
+import static org.opends.server.util.ServerConstants.*;
+import static org.opends.server.util.StaticUtils.*;
+
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileWriter;
@@ -31,6 +38,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.StringTokenizer;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -44,11 +52,10 @@
 import javax.security.sasl.Sasl;
 import javax.security.sasl.SaslClient;
 
-import com.forgerock.opendj.cli.ClientException;
-import com.forgerock.opendj.cli.ConsoleApplication;
-import com.forgerock.opendj.cli.ReturnCode;
-
 import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg0;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
 import org.forgerock.opendj.ldap.ByteSequence;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DecodeException;
@@ -61,12 +68,9 @@
 import org.opends.server.types.LDAPException;
 import org.opends.server.util.Base64;
 
-import static com.forgerock.opendj.cli.ArgumentConstants.*;
-
-import static org.opends.messages.ToolMessages.*;
-import static org.opends.server.protocols.ldap.LDAPConstants.*;
-import static org.opends.server.util.ServerConstants.*;
-import static org.opends.server.util.StaticUtils.*;
+import com.forgerock.opendj.cli.ClientException;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.ReturnCode;
 
 /**
  * This class provides a generic interface that LDAP clients can use to perform
@@ -88,42 +92,32 @@
 public class LDAPAuthenticationHandler
        implements PrivilegedExceptionAction<Object>, CallbackHandler
 {
-  /** The bind DN for GSSAPI authentication. */
-  private ByteSequence gssapiBindDN;
-
   /** The LDAP reader that will be used to read data from the server. */
   private final LDAPReader reader;
-
   /** The LDAP writer that will be used to send data to the server. */
   private final LDAPWriter writer;
 
-  /**
-   * The atomic integer that will be used to obtain message IDs for request
-   * messages.
-   */
+  /** The atomic integer that will be used to obtain message IDs for request messages. */
   private final AtomicInteger nextMessageID;
 
   /** An array filled with the inner pad byte. */
   private byte[] iPad;
-
   /** An array filled with the outer pad byte. */
   private byte[] oPad;
 
-  /** The authentication password for GSSAPI authentication. */
-  private char[] gssapiAuthPW;
-
   /** The message digest that will be used to create MD5 hashes. */
   private MessageDigest md5Digest;
-
   /** The secure random number generator for use by this authentication handler. */
   private SecureRandom secureRandom;
 
+  /** The bind DN for GSSAPI authentication. */
+  private ByteSequence gssapiBindDN;
   /** The authentication ID for GSSAPI authentication. */
   private String gssapiAuthID;
-
   /** The authorization ID for GSSAPI authentication. */
   private String gssapiAuthzID;
-
+  /** The authentication password for GSSAPI authentication. */
+  private char[] gssapiAuthPW;
   /** The quality of protection for GSSAPI authentication. */
   private String gssapiQoP;
 
@@ -200,36 +194,23 @@
    *          specified SASL mechanism, mapped from the property names to their
    *          corresponding descriptions.
    */
-  public static LinkedHashMap<String,LocalizableMessage> getSASLProperties(
-          String mechanism)
+  public static Map<String, LocalizableMessage> getSASLProperties(String mechanism)
   {
-    String upperName = toUpperCase(mechanism);
-    if (upperName.equals(SASL_MECHANISM_ANONYMOUS))
+    switch (toUpperCase(mechanism))
     {
+    case SASL_MECHANISM_ANONYMOUS:
       return getSASLAnonymousProperties();
-    }
-    else if (upperName.equals(SASL_MECHANISM_CRAM_MD5))
-    {
+    case SASL_MECHANISM_CRAM_MD5:
       return getSASLCRAMMD5Properties();
-    }
-    else if (upperName.equals(SASL_MECHANISM_DIGEST_MD5))
-    {
+    case SASL_MECHANISM_DIGEST_MD5:
       return getSASLDigestMD5Properties();
-    }
-    else if (upperName.equals(SASL_MECHANISM_EXTERNAL))
-    {
+    case SASL_MECHANISM_EXTERNAL:
       return getSASLExternalProperties();
-    }
-    else if (upperName.equals(SASL_MECHANISM_GSSAPI))
-    {
+    case SASL_MECHANISM_GSSAPI:
       return getSASLGSSAPIProperties();
-    }
-    else if (upperName.equals(SASL_MECHANISM_PLAIN))
-    {
+    case SASL_MECHANISM_PLAIN:
       return getSASLPlainProperties();
-    }
-    else
-    {
+    default:
       // This is an unsupported mechanism.
       return null;
     }
@@ -279,21 +260,26 @@
         bindPassword = ByteString.empty();
     }
 
-
     // Make sure that critical elements aren't null.
     if (bindDN == null)
     {
       bindDN = ByteString.empty();
     }
 
+    sendSimpleBindRequest(ldapVersion, bindDN, bindPassword, requestControls);
 
-    // Create the bind request and send it to the server.
+    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
+    responseControls.addAll(responseMessage.getControls());
+    checkConnected(responseMessage);
+    return checkSuccessfulSimpleBind(responseMessage);
+  }
+
+  private void sendSimpleBindRequest(int ldapVersion, ByteSequence bindDN, ByteSequence bindPassword,
+      List<Control> requestControls) throws ClientException
+  {
     BindRequestProtocolOp bindRequest =
-         new BindRequestProtocolOp(bindDN.toByteString(), ldapVersion,
-             bindPassword.toByteString());
-    LDAPMessage bindRequestMessage =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
-                         requestControls);
+        new BindRequestProtocolOp(bindDN.toByteString(), ldapVersion, bindPassword.toByteString());
+    LDAPMessage bindRequestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, requestControls);
 
     try
     {
@@ -301,87 +287,44 @@
     }
     catch (IOException ioe)
     {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
+      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(ioe));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
     }
     catch (Exception e)
     {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(e));
+      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(e));
       throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
     }
-
-
-    // Read the response from the server.
-    LDAPMessage responseMessage;
-    try
-    {
-      responseMessage = reader.readMessage();
-      if (responseMessage == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
-      throw new ClientException(
-          ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(
-          ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
-
-    // See if there are any controls in the response.  If so, then add them to
-    // the response controls list.
-    List<Control> respControls = responseMessage.getControls();
-    if (respControls != null && !respControls.isEmpty())
-    {
-      responseControls.addAll(respControls);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    generateError(responseMessage);
-
-
-    BindResponseProtocolOp bindResponse =
-         responseMessage.getBindResponseProtocolOp();
-    int resultCode = bindResponse.getResultCode();
-    if (resultCode == ReturnCode.SUCCESS.get())
-    {
-      // FIXME -- Need to look for things like password expiration warning,
-      // reset notice, etc.
-      return null;
-    }
-
-    // FIXME -- Add support for referrals.
-
-    LocalizableMessage message = ERR_LDAPAUTH_SIMPLE_BIND_FAILED.get();
-    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
-                            message, bindResponse.getMatchedDN(), null);
   }
 
+  private BindResponseProtocolOp checkSuccessfulBind(LDAPMessage responseMessage, String saslMechanism)
+      throws LDAPException
+  {
+    BindResponseProtocolOp bindResponse = responseMessage.getBindResponseProtocolOp();
+    int resultCode = bindResponse.getResultCode();
+    if (resultCode != ReturnCode.SUCCESS.get())
+    {
+      // FIXME -- Add support for referrals.
+      LocalizableMessage message = ERR_LDAPAUTH_SASL_BIND_FAILED.get(saslMechanism);
+      throw new LDAPException(resultCode, bindResponse.getErrorMessage(), message, bindResponse.getMatchedDN(), null);
+    }
+    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
+    return bindResponse;
+  }
 
+  private String checkSuccessfulSimpleBind(LDAPMessage responseMessage) throws LDAPException
+  {
+    BindResponseProtocolOp bindResponse = responseMessage.getBindResponseProtocolOp();
+    int resultCode = bindResponse.getResultCode();
+    if (resultCode != ReturnCode.SUCCESS.get())
+    {
+      // FIXME -- Add support for referrals.
+      LocalizableMessage message = ERR_LDAPAUTH_SIMPLE_BIND_FAILED.get();
+      throw new LDAPException(resultCode, bindResponse.getErrorMessage(), message, bindResponse.getMatchedDN(), null);
+    }
+    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
+    return null;
+  }
 
   /**
    * Processes a SASL bind using the provided information.  If the bind fails,
@@ -431,49 +374,29 @@
     if (mechanism == null || mechanism.length() == 0)
     {
       LocalizableMessage message = ERR_LDAPAUTH_NO_SASL_MECHANISM.get();
-      throw new ClientException(
-          ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
+      throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
     }
 
 
-    // Look at the mechanism name and call the appropriate method to process
-    // the request.
+    // Look at the mechanism name and call the appropriate method to process the request.
     saslMechanism = toUpperCase(mechanism);
-    if (saslMechanism.equals(SASL_MECHANISM_ANONYMOUS))
+    switch (saslMechanism)
     {
-      return doSASLAnonymous(bindDN, saslProperties, requestControls,
-                             responseControls);
-    }
-    else if (saslMechanism.equals(SASL_MECHANISM_CRAM_MD5))
-    {
-      return doSASLCRAMMD5(bindDN, bindPassword, saslProperties,
-                           requestControls, responseControls);
-    }
-    else if (saslMechanism.equals(SASL_MECHANISM_DIGEST_MD5))
-    {
-      return doSASLDigestMD5(bindDN, bindPassword, saslProperties,
-                             requestControls, responseControls);
-    }
-    else if (saslMechanism.equals(SASL_MECHANISM_EXTERNAL))
-    {
-      return doSASLExternal(bindDN, saslProperties, requestControls,
-                            responseControls);
-    }
-    else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
-    {
-      return doSASLGSSAPI(bindDN, bindPassword, saslProperties, requestControls,
-                          responseControls);
-    }
-    else if (saslMechanism.equals(SASL_MECHANISM_PLAIN))
-    {
-      return doSASLPlain(bindDN, bindPassword, saslProperties, requestControls,
-                         responseControls);
-    }
-    else
-    {
+    case SASL_MECHANISM_ANONYMOUS:
+      return doSASLAnonymous(bindDN, saslProperties, requestControls, responseControls);
+    case SASL_MECHANISM_CRAM_MD5:
+      return doSASLCRAMMD5(bindDN, bindPassword, saslProperties, requestControls, responseControls);
+    case SASL_MECHANISM_DIGEST_MD5:
+      return doSASLDigestMD5(bindDN, bindPassword, saslProperties, requestControls, responseControls);
+    case SASL_MECHANISM_EXTERNAL:
+      return doSASLExternal(bindDN, saslProperties, requestControls, responseControls);
+    case SASL_MECHANISM_GSSAPI:
+      return doSASLGSSAPI(bindDN, bindPassword, saslProperties, requestControls, responseControls);
+    case SASL_MECHANISM_PLAIN:
+      return doSASLPlain(bindDN, bindPassword, saslProperties, requestControls, responseControls);
+    default:
       LocalizableMessage message = ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mechanism);
-      throw new ClientException(
-          ReturnCode.CLIENT_SIDE_AUTH_UNKNOWN, message);
+      throw new ClientException(ReturnCode.CLIENT_SIDE_AUTH_UNKNOWN, message);
     }
   }
 
@@ -502,7 +425,7 @@
    * @throws  LDAPException  If the bind fails or some other server-side problem
    *                         occurs during processing.
    */
-  public String doSASLAnonymous(ByteSequence bindDN,
+  private String doSASLAnonymous(ByteSequence bindDN,
                      Map<String,List<String>> saslProperties,
                      List<Control> requestControls,
                      List<Control> responseControls)
@@ -510,151 +433,38 @@
   {
     String trace = null;
 
-
-    // Evaluate the properties provided.  The only one we'll allow is the trace
-    // property, but it is not required.
-    if (saslProperties == null || saslProperties.isEmpty())
+    // The only allowed property is the trace property, but it is not required.
+    if (saslProperties != null)
     {
-      // This is fine because there are no required properties for this mechanism.
-    }
-    else
-    {
-      for (String name : saslProperties.keySet())
+      for (Entry<String, List<String>> entry : saslProperties.entrySet())
       {
+        String name = entry.getKey();
+        List<String> values = entry.getValue();
         if (name.equalsIgnoreCase(SASL_PROPERTY_TRACE))
         {
           // This is acceptable, and we'll take any single value.
-          List<String> values = saslProperties.get(name);
-          Iterator<String> iterator = values.iterator();
-          if (iterator.hasNext())
-          {
-            trace = iterator.next();
-
-            if (iterator.hasNext())
-            {
-              LocalizableMessage message = ERR_LDAPAUTH_TRACE_SINGLE_VALUED.get();
-              throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
-            }
-          }
+          trace = getSingleValue(values, ERR_LDAPAUTH_TRACE_SINGLE_VALUED);
         }
         else
         {
           LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
               name, SASL_MECHANISM_ANONYMOUS);
-          throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                                    message);
+          throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
         }
       }
     }
 
-
     // Construct the bind request and send it to the server.
-    ByteString saslCredentials;
-    if (trace == null)
-    {
-      saslCredentials = null;
-    }
-    else
-    {
-      saslCredentials = ByteString.valueOfUtf8(trace);
-    }
+    ByteString saslCredentials = trace != null ? ByteString.valueOfUtf8(trace) : null;
+    sendBindRequest(SASL_MECHANISM_ANONYMOUS, bindDN, saslCredentials, requestControls);
 
-    BindRequestProtocolOp bindRequest =
-         new BindRequestProtocolOp(bindDN.toByteString(),
-             SASL_MECHANISM_ANONYMOUS, saslCredentials);
-    LDAPMessage requestMessage =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
-                         requestControls);
-
-    try
-    {
-      writer.writeMessage(requestMessage);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-          SASL_MECHANISM_ANONYMOUS, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-          SASL_MECHANISM_ANONYMOUS, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
-    }
-
-
-    // Read the response from the server.
-    LDAPMessage responseMessage;
-    try
-    {
-      responseMessage = reader.readMessage();
-      if (responseMessage == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
-
-    // See if there are any controls in the response.  If so, then add them to
-    // the response controls list.
-    List<Control> respControls = responseMessage.getControls();
-    if (respControls != null && ! respControls.isEmpty())
-    {
-      responseControls.addAll(respControls);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    generateError(responseMessage);
-
-
-    BindResponseProtocolOp bindResponse =
-         responseMessage.getBindResponseProtocolOp();
-    int resultCode = bindResponse.getResultCode();
-    if (resultCode == ReturnCode.SUCCESS.get())
-    {
-      // FIXME -- Need to look for things like password expiration warning,
-      // reset notice, etc.
-      return null;
-    }
-
-    // FIXME -- Add support for referrals.
-
-    LocalizableMessage message =
-        ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_ANONYMOUS);
-    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
-                            message, bindResponse.getMatchedDN(), null);
+    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
+    responseControls.addAll(responseMessage.getControls());
+    checkConnected(responseMessage);
+    checkSuccessfulBind(responseMessage, SASL_MECHANISM_ANONYMOUS);
+    return null;
   }
 
-
-
   /**
    * Retrieves the set of properties that a client may provide when performing a
    * SASL ANONYMOUS bind, mapped from the property names to their corresponding
@@ -664,7 +474,7 @@
    *          SASL ANONYMOUS bind, mapped from the property names to their
    *          corresponding descriptions.
    */
-  public static LinkedHashMap<String, LocalizableMessage> getSASLAnonymousProperties()
+  private static LinkedHashMap<String, LocalizableMessage> getSASLAnonymousProperties()
   {
     LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1);
 
@@ -701,7 +511,7 @@
    * @throws  LDAPException  If the bind fails or some other server-side problem
    *                         occurs during processing.
    */
-  public String doSASLCRAMMD5(ByteSequence bindDN,
+  private String doSASLCRAMMD5(ByteSequence bindDN,
                      ByteSequence bindPassword,
                      Map<String,List<String>> saslProperties,
                      List<Control> requestControls,
@@ -721,20 +531,21 @@
               ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
     }
 
-    for (String name : saslProperties.keySet())
+    for (Entry<String, List<String>> entry : saslProperties.entrySet())
     {
+      String name = entry.getKey();
+      List<String> values = entry.getValue();
       String lowerName = toLowerCase(name);
 
       if (lowerName.equals(SASL_PROPERTY_AUTHID))
       {
-        authID = getAuthID(saslProperties, authID, name);
+        authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
       }
       else
       {
         LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
             name, SASL_MECHANISM_CRAM_MD5);
-        throw new ClientException(
-                ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
+        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
       }
     }
 
@@ -755,108 +566,13 @@
         bindPassword = ByteString.empty();
     }
 
+    sendInitialBindRequest(SASL_MECHANISM_CRAM_MD5, bindDN);
 
-    // Construct the initial bind request to send to the server.  In this case,
-    // we'll simply indicate that we want to use CRAM-MD5 so the server will
-    // send us the challenge.
-    BindRequestProtocolOp bindRequest1 =
-         new BindRequestProtocolOp(bindDN.toByteString(),
-             SASL_MECHANISM_CRAM_MD5, null);
-    // FIXME -- Should we include request controls in both stages or just the
-    // second stage?
-    LDAPMessage requestMessage1 =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1);
+    LDAPMessage responseMessage1 =
+        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE, SASL_MECHANISM_CRAM_MD5);
+    checkConnected(responseMessage1);
 
-    try
-    {
-      writer.writeMessage(requestMessage1);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
-    }
-
-
-    // Read the response from the server.
-    LDAPMessage responseMessage1;
-    try
-    {
-      responseMessage1 = reader.readMessage();
-      if (responseMessage1 == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
-              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    switch (responseMessage1.getProtocolOpType())
-    {
-      case OP_TYPE_BIND_RESPONSE:
-        // We'll deal with this later.
-        break;
-
-      case OP_TYPE_EXTENDED_RESPONSE:
-        ExtendedResponseProtocolOp extendedResponse =
-             responseMessage1.getExtendedResponseProtocolOp();
-        String responseOID = extendedResponse.getOID();
-        if (responseOID != null &&
-            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
-              get(extendedResponse.getResultCode(),
-                  extendedResponse.getErrorMessage());
-          throw new LDAPException(extendedResponse.getResultCode(), message);
-        }
-        else
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
-          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-        }
-
-      default:
-        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage1.getProtocolOp());
-        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-    }
-
-
-    // Make sure that the bind response has the "SASL bind in progress" result
-    // code.
+    // Make sure that the bind response has the "SASL bind in progress" result code.
     BindResponseProtocolOp bindResponse1 =
          responseMessage1.getBindResponseProtocolOp();
     int resultCode1 = bindResponse1.getResultCode();
@@ -885,158 +601,76 @@
       throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
     }
 
+    // Use the provided password and credentials to generate the CRAM-MD5 response.
+    String salsCredentials = authID + ' ' + generateCRAMMD5Digest(bindPassword, serverChallenge);
+    sendSecondBindRequest(SASL_MECHANISM_CRAM_MD5, bindDN, salsCredentials, requestControls);
 
-    // Use the provided password and credentials to generate the CRAM-MD5
-    // response.
-    StringBuilder buffer = new StringBuilder();
-    buffer.append(authID);
-    buffer.append(' ');
-    buffer.append(generateCRAMMD5Digest(bindPassword, serverChallenge));
+    LDAPMessage responseMessage2 =
+        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE, SASL_MECHANISM_CRAM_MD5);
+    responseControls.addAll(responseMessage2.getControls());
+    checkConnected(responseMessage2);
+    checkSuccessfulBind(responseMessage2, SASL_MECHANISM_CRAM_MD5);
+    return null;
+  }
 
-
-    // Create and send the second bind request to the server.
-    BindRequestProtocolOp bindRequest2 =
-         new BindRequestProtocolOp(bindDN.toByteString(),
-             SASL_MECHANISM_CRAM_MD5, ByteString.valueOfUtf8(buffer.toString()));
-    LDAPMessage requestMessage2 =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2,
-                         requestControls);
+  /**
+   * Construct the initial bind request to send to the server. We'll simply indicate the SASL
+   * mechanism we want to use so the server will send us the challenge.
+   */
+  private void sendInitialBindRequest(String saslMechanism, ByteSequence bindDN) throws ClientException
+  {
+    // FIXME -- Should we include request controls in both stages or just the second stage?
+    BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, null);
+    LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
 
     try
     {
-      writer.writeMessage(requestMessage2);
+      writer.writeMessage(requestMessage);
     }
     catch (IOException ioe)
     {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
+      LocalizableMessage message =
+          ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
     }
     catch (Exception e)
     {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
+      LocalizableMessage message =
+          ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(saslMechanism, getExceptionMessage(e));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
     }
+  }
 
-
-    // Read the response from the server.
-    LDAPMessage responseMessage2;
+  private LDAPMessage readBindResponse(Arg2<Object, Object> errCannotReadBindResponse, String saslMechanism)
+      throws ClientException
+  {
     try
     {
-      responseMessage2 = reader.readMessage();
-      if (responseMessage2 == null)
+      LDAPMessage responseMessage = reader.readMessage();
+      if (responseMessage != null)
       {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
+        return responseMessage;
       }
+      LocalizableMessage message = ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message);
     }
     catch (DecodeException | LDAPException e)
     {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
-              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
+      LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(e));
       throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
     }
     catch (IOException ioe)
     {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
+      LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(ioe));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
     }
     catch (Exception e)
     {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
-          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
+      LocalizableMessage message = errCannotReadBindResponse.get(saslMechanism, getExceptionMessage(e));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
     }
-
-
-    // See if there are any controls in the response.  If so, then add them to
-    // the response controls list.
-    List<Control> respControls = responseMessage2.getControls();
-    if (respControls != null && ! respControls.isEmpty())
-    {
-      responseControls.addAll(respControls);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    switch (responseMessage2.getProtocolOpType())
-    {
-      case OP_TYPE_BIND_RESPONSE:
-        // We'll deal with this later.
-        break;
-
-      case OP_TYPE_EXTENDED_RESPONSE:
-        ExtendedResponseProtocolOp extendedResponse =
-             responseMessage2.getExtendedResponseProtocolOp();
-        String responseOID = extendedResponse.getOID();
-        if (responseOID != null &&
-            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
-              get(extendedResponse.getResultCode(),
-                  extendedResponse.getErrorMessage());
-          throw new LDAPException(extendedResponse.getResultCode(), message);
-        }
-        else
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
-          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-        }
-
-      default:
-        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage2.getProtocolOp());
-        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-    }
-
-
-    BindResponseProtocolOp bindResponse2 =
-         responseMessage2.getBindResponseProtocolOp();
-    int resultCode2 = bindResponse2.getResultCode();
-    if (resultCode2 == ReturnCode.SUCCESS.get())
-    {
-      // FIXME -- Need to look for things like password expiration warning,
-      // reset notice, etc.
-      return null;
-    }
-
-    // FIXME -- Add support for referrals.
-
-    LocalizableMessage message =
-        ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_CRAM_MD5);
-    throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(),
-                            message, bindResponse2.getMatchedDN(), null);
   }
 
-  private String getAuthID(Map<String, List<String>> saslProperties, String authID, String name) throws ClientException
-  {
-    List<String> values = saslProperties.get(name);
-    Iterator<String> iterator = values.iterator();
-    if (iterator.hasNext())
-    {
-      authID = iterator.next();
-
-      if (iterator.hasNext())
-      {
-        LocalizableMessage message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
-      }
-    }
-    return authID;
-  }
-
-
-
   /**
    * Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication
    * with the given information.
@@ -1141,7 +775,7 @@
    *          SASL CRAM-MD5 bind, mapped from the property names to their
    *          corresponding descriptions.
    */
-  public static LinkedHashMap<String,LocalizableMessage> getSASLCRAMMD5Properties()
+  private static LinkedHashMap<String, LocalizableMessage> getSASLCRAMMD5Properties()
   {
     LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1);
 
@@ -1178,7 +812,7 @@
    * @throws  LDAPException  If the bind fails or some other server-side problem
    *                         occurs during processing.
    */
-  public String doSASLDigestMD5(ByteSequence bindDN,
+  private String doSASLDigestMD5(ByteSequence bindDN,
                      ByteSequence bindPassword,
                      Map<String,List<String>> saslProperties,
                      List<Control> requestControls,
@@ -1202,17 +836,18 @@
       throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
     }
 
-    for (String name : saslProperties.keySet())
+    for (Entry<String, List<String>> entry : saslProperties.entrySet())
     {
+      String name = entry.getKey();
+      List<String> values = entry.getValue();
       String lowerName = toLowerCase(name);
 
       if (lowerName.equals(SASL_PROPERTY_AUTHID))
       {
-        authID = getAuthID(saslProperties, authID, name);
+        authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
       }
       else if (lowerName.equals(SASL_PROPERTY_REALM))
       {
-        List<String> values = saslProperties.get(name);
         Iterator<String> iterator = values.iterator();
         if (iterator.hasNext())
         {
@@ -1229,7 +864,6 @@
       }
       else if (lowerName.equals(SASL_PROPERTY_QOP))
       {
-        List<String> values = saslProperties.get(name);
         Iterator<String> iterator = values.iterator();
         if (iterator.hasNext())
         {
@@ -1264,42 +898,17 @@
       }
       else if (lowerName.equals(SASL_PROPERTY_DIGEST_URI))
       {
-        List<String> values = saslProperties.get(name);
-        Iterator<String> iterator = values.iterator();
-        if (iterator.hasNext())
-        {
-          digestURI = toLowerCase(iterator.next());
-
-          if (iterator.hasNext())
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_DIGEST_URI_SINGLE_VALUED.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                                      message);
-          }
-        }
+        digestURI = toLowerCase(getSingleValue(values, ERR_LDAPAUTH_DIGEST_URI_SINGLE_VALUED));
       }
       else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
       {
-        List<String> values = saslProperties.get(name);
-        Iterator<String> iterator = values.iterator();
-        if (iterator.hasNext())
-        {
-          authzID = toLowerCase(iterator.next());
-
-          if (iterator.hasNext())
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                                      message);
-          }
-        }
+        authzID = toLowerCase(getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED));
       }
       else
       {
         LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
             name, SASL_MECHANISM_DIGEST_MD5);
-        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                message);
+        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
       }
     }
 
@@ -1321,108 +930,13 @@
     }
 
 
-    // Construct the initial bind request to send to the server.  In this case,
-    // we'll simply indicate that we want to use DIGEST-MD5 so the server will
-    // send us the challenge.
-    BindRequestProtocolOp bindRequest1 =
-         new BindRequestProtocolOp(bindDN.toByteString(),
-             SASL_MECHANISM_DIGEST_MD5, null);
-    // FIXME -- Should we include request controls in both stages or just the
-    // second stage?
-    LDAPMessage requestMessage1 =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1);
+    sendInitialBindRequest(SASL_MECHANISM_DIGEST_MD5, bindDN);
 
-    try
-    {
-      writer.writeMessage(requestMessage1);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
-                                message, e);
-    }
+    LDAPMessage responseMessage1 =
+        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE, SASL_MECHANISM_DIGEST_MD5);
+    checkConnected(responseMessage1);
 
-
-    // Read the response from the server.
-    LDAPMessage responseMessage1;
-    try
-    {
-      responseMessage1 = reader.readMessage();
-      if (responseMessage1 == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
-              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    switch (responseMessage1.getProtocolOpType())
-    {
-      case OP_TYPE_BIND_RESPONSE:
-        // We'll deal with this later.
-        break;
-
-      case OP_TYPE_EXTENDED_RESPONSE:
-        ExtendedResponseProtocolOp extendedResponse =
-             responseMessage1.getExtendedResponseProtocolOp();
-        String responseOID = extendedResponse.getOID();
-        if (responseOID != null &&
-            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
-              get(extendedResponse.getResultCode(),
-                  extendedResponse.getErrorMessage());
-          throw new LDAPException(extendedResponse.getResultCode(), message);
-        }
-        else
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
-          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-        }
-
-      default:
-        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage1.getProtocolOp());
-        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-    }
-
-
-    // Make sure that the bind response has the "SASL bind in progress" result
-    // code.
+    // Make sure that the bind response has the "SASL bind in progress" result code.
     BindResponseProtocolOp bindResponse1 =
          responseMessage1.getBindResponseProtocolOp();
     int resultCode1 = bindResponse1.getResultCode();
@@ -1591,163 +1105,33 @@
 
     // Generate the SASL credentials for the second bind request.
     StringBuilder credBuffer = new StringBuilder();
-    credBuffer.append("username=\"");
-    credBuffer.append(authID);
-    credBuffer.append("\"");
-
+    credBuffer.append("username=\"").append(authID).append("\"");
     if (realm != null)
     {
-      credBuffer.append(",realm=\"");
-      credBuffer.append(realm);
-      credBuffer.append("\"");
+      credBuffer.append(",realm=\"").append(realm).append("\"");
     }
-
-    credBuffer.append(",nonce=\"");
-    credBuffer.append(nonce);
-    credBuffer.append("\",cnonce=\"");
-    credBuffer.append(cnonce);
-    credBuffer.append("\",nc=");
-    credBuffer.append(nonceCount);
-    credBuffer.append(",qop=");
-    credBuffer.append(qop);
-    credBuffer.append(",digest-uri=\"");
-    credBuffer.append(digestURI);
-    credBuffer.append("\",response=");
-    credBuffer.append(responseDigest);
-
+    credBuffer.append(",nonce=\"").append(nonce);
+    credBuffer.append("\",cnonce=\"").append(cnonce);
+    credBuffer.append("\",nc=").append(nonceCount);
+    credBuffer.append(",qop=").append(qop);
+    credBuffer.append(",digest-uri=\"").append(digestURI);
+    credBuffer.append("\",response=").append(responseDigest);
     if (useUTF8)
     {
       credBuffer.append(",charset=utf-8");
     }
-
     if (authzID != null)
     {
-      credBuffer.append(",authzid=\"");
-      credBuffer.append(authzID);
-      credBuffer.append("\"");
+      credBuffer.append(",authzid=\"").append(authzID).append("\"");
     }
 
+    sendSecondBindRequest(SASL_MECHANISM_DIGEST_MD5, bindDN, credBuffer.toString(), requestControls);
 
-    // Generate and send the second bind request.
-    BindRequestProtocolOp bindRequest2 =
-         new BindRequestProtocolOp(bindDN.toByteString(),
-             SASL_MECHANISM_DIGEST_MD5,
-             ByteString.valueOfUtf8(credBuffer.toString()));
-    LDAPMessage requestMessage2 =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2,
-                         requestControls);
-
-    try
-    {
-      writer.writeMessage(requestMessage2);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
-                                message, e);
-    }
-
-
-    // Read the response from the server.
-    LDAPMessage responseMessage2;
-    try
-    {
-      responseMessage2 = reader.readMessage();
-      if (responseMessage2 == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
-              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
-          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
-
-    // See if there are any controls in the response.  If so, then add them to
-    // the response controls list.
-    List<Control> respControls = responseMessage2.getControls();
-    if (respControls != null && ! respControls.isEmpty())
-    {
-      responseControls.addAll(respControls);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    switch (responseMessage2.getProtocolOpType())
-    {
-      case OP_TYPE_BIND_RESPONSE:
-        // We'll deal with this later.
-        break;
-
-      case OP_TYPE_EXTENDED_RESPONSE:
-        ExtendedResponseProtocolOp extendedResponse =
-             responseMessage2.getExtendedResponseProtocolOp();
-        String responseOID = extendedResponse.getOID();
-        if (responseOID != null &&
-            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
-              get(extendedResponse.getResultCode(),
-                  extendedResponse.getErrorMessage());
-          throw new LDAPException(extendedResponse.getResultCode(), message);
-        }
-        else
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
-          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-        }
-
-      default:
-        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage2.getProtocolOp());
-        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-    }
-
-
-    BindResponseProtocolOp bindResponse2 =
-         responseMessage2.getBindResponseProtocolOp();
-    int resultCode2 = bindResponse2.getResultCode();
-    if (resultCode2 != ReturnCode.SUCCESS.get())
-    {
-      // FIXME -- Add support for referrals.
-
-      LocalizableMessage message =
-          ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_DIGEST_MD5);
-      throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(),
-                              message, bindResponse2.getMatchedDN(),
-                              null);
-    }
+    LDAPMessage responseMessage2 =
+        readBindResponse(ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE, SASL_MECHANISM_DIGEST_MD5);
+    responseControls.addAll(responseMessage2.getControls());
+    checkConnected(responseMessage2);
+    BindResponseProtocolOp bindResponse2 = checkSuccessfulBind(responseMessage2, SASL_MECHANISM_DIGEST_MD5);
 
 
     // Make sure that the bind response included server SASL credentials with
@@ -1802,12 +1186,34 @@
               ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
     }
 
-    // FIXME -- Need to look for things like password expiration warning,
-    // reset notice, etc.
+    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
     return null;
   }
 
+  private void sendSecondBindRequest(String saslMechanism, ByteSequence bindDN, String saslCredentials,
+      List<Control> requestControls) throws ClientException
+  {
+    // Generate and send the second bind request.
+    BindRequestProtocolOp bindRequest2 =
+        new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, ByteString.valueOfUtf8(saslCredentials));
+    LDAPMessage requestMessage2 = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2, requestControls);
 
+    try
+    {
+      writer.writeMessage(requestMessage2);
+    }
+    catch (IOException ioe)
+    {
+      LocalizableMessage message =
+          ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
+    }
+    catch (Exception e)
+    {
+      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(saslMechanism, getExceptionMessage(e));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
+    }
+  }
 
   /**
    * Reads the next token from the provided credentials string using the
@@ -1891,14 +1297,11 @@
       {
         // If this is a quoted string, then this comma is part of the token.
         // Otherwise, it's the end of the token.
-        if (isQuoted)
-        {
-          token.append(c);
-        }
-        else
+        if (!isQuoted)
         {
           break;
         }
+        token.append(c);
       }
       else if (c == '"')
       {
@@ -1911,23 +1314,17 @@
             // We have hit the end of the string, so this is fine.
             break;
           }
+          char c2 = credentials.charAt(pos++);
+          if (c2 == ',')
+          {
+            // We have hit the end of the token, so this is fine.
+            break;
+          }
           else
           {
-            char c2 = credentials.charAt(pos++);
-            if (c2 == ',')
-            {
-              // We have hit the end of the token, so this is fine.
-              break;
-            }
-            else
-            {
-              // We found the closing quote before the end of the token.  This
-              // is not fine.
-              LocalizableMessage message =
-                  ERR_LDAPAUTH_DIGESTMD5_INVALID_CLOSING_QUOTE_POS.get(pos-2);
-              throw new LDAPException(ReturnCode.INVALID_CREDENTIALS.get(),
-                                      message);
-            }
+            // We found the closing quote before the end of the token. This is not fine.
+            LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_CLOSING_QUOTE_POS.get(pos - 2);
+            throw new LDAPException(ReturnCode.INVALID_CREDENTIALS.get(), message);
           }
         }
         else
@@ -2028,21 +1425,14 @@
       }
     }
 
-
     // Get a hash of "username:realm:password".
-    StringBuilder a1String1 = new StringBuilder();
-    a1String1.append(authID);
-    a1String1.append(':');
-    a1String1.append((realm == null) ? "" : realm);
-    a1String1.append(':');
-
-    byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
+    String a1String1 = authID + ':' + ((realm == null) ? "" : realm) + ':';
+    byte[] a1Bytes1a = a1String1.getBytes(charset);
     byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length()];
     System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
     password.copyTo(a1Bytes1, a1Bytes1a.length);
     byte[] urpHash = md5Digest.digest(a1Bytes1);
 
-
     // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
     StringBuilder a1String2 = new StringBuilder();
     a1String2.append(':');
@@ -2060,37 +1450,20 @@
     System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length);
     byte[] a1Hash = md5Digest.digest(a1Bytes2);
 
-
     // Next, get a hash of "AUTHENTICATE:digesturi".
     byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset);
     byte[] a2Hash  = md5Digest.digest(a2Bytes);
 
-
     // Get hex string representations of the last two hashes.
     String a1HashHex = getHexString(a1Hash);
     String a2HashHex = getHexString(a2Hash);
 
-
     // Put together the final string to hash, consisting of
     // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
-    StringBuilder kdStr = new StringBuilder();
-    kdStr.append(a1HashHex);
-    kdStr.append(':');
-    kdStr.append(nonce);
-    kdStr.append(':');
-    kdStr.append(nonceCount);
-    kdStr.append(':');
-    kdStr.append(cnonce);
-    kdStr.append(':');
-    kdStr.append(qop);
-    kdStr.append(':');
-    kdStr.append(a2HashHex);
-
-    return getHexString(md5Digest.digest(kdStr.toString().getBytes(charset)));
+    String kdStr = a1HashHex + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + a2HashHex;
+    return getHexString(md5Digest.digest(kdStr.getBytes(charset)));
   }
 
-
-
   /**
    * Generates the appropriate DIGEST-MD5 rspauth digest using the provided
    * information.
@@ -2118,7 +1491,7 @@
    * @throws  UnsupportedEncodingException  If the specified character set is
    *                                        invalid for some reason.
    */
-  public byte[] generateDigestMD5RspAuth(String authID, String authzID,
+  private byte[] generateDigestMD5RspAuth(String authID, String authzID,
                                          ByteSequence password, String realm,
                                          String nonce, String cnonce,
                                          String nonceCount, String digestURI,
@@ -2126,13 +1499,9 @@
          throws UnsupportedEncodingException
   {
     // First, get a hash of "username:realm:password".
-    StringBuilder a1String1 = new StringBuilder();
-    a1String1.append(authID);
-    a1String1.append(':');
-    a1String1.append(realm);
-    a1String1.append(':');
+    String a1String1 = authID + ':' + realm + ':';
 
-    byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
+    byte[] a1Bytes1a = a1String1.getBytes(charset);
     byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length()];
     System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
     password.copyTo(a1Bytes1, a1Bytes1a.length);
@@ -2172,26 +1541,12 @@
     String a1HashHex = getHexString(a1Hash);
     String a2HashHex = getHexString(a2Hash);
 
-
     // Put together the final string to hash, consisting of
     // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
-    StringBuilder kdStr = new StringBuilder();
-    kdStr.append(a1HashHex);
-    kdStr.append(':');
-    kdStr.append(nonce);
-    kdStr.append(':');
-    kdStr.append(nonceCount);
-    kdStr.append(':');
-    kdStr.append(cnonce);
-    kdStr.append(':');
-    kdStr.append(qop);
-    kdStr.append(':');
-    kdStr.append(a2HashHex);
-    return md5Digest.digest(kdStr.toString().getBytes(charset));
+    String kdStr = a1HashHex + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + a2HashHex;
+    return md5Digest.digest(kdStr.getBytes(charset));
   }
 
-
-
   /**
    * Retrieves a hexadecimal string representation of the contents of the
    * provided byte array.
@@ -2224,7 +1579,7 @@
    *          SASL DIGEST-MD5 bind, mapped from the property names to their
    *          corresponding descriptions.
    */
-  public static LinkedHashMap<String,LocalizableMessage> getSASLDigestMD5Properties()
+  private static LinkedHashMap<String, LocalizableMessage> getSASLDigestMD5Properties()
   {
     LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(5);
 
@@ -2285,111 +1640,11 @@
     }
 
 
-    // Construct the bind request and send it to the server.
-    BindRequestProtocolOp bindRequest =
-         new BindRequestProtocolOp(bindDN.toByteString(),
-             SASL_MECHANISM_EXTERNAL, null);
-    LDAPMessage requestMessage =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
-                         requestControls);
+    sendBindRequest(SASL_MECHANISM_EXTERNAL, bindDN, null, requestControls);
 
-    try
-    {
-      writer.writeMessage(requestMessage);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-          SASL_MECHANISM_EXTERNAL, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-          SASL_MECHANISM_EXTERNAL, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
-                                message, e);
-    }
-
-
-    // Read the response from the server.
-    LDAPMessage responseMessage;
-    try
-    {
-      responseMessage = reader.readMessage();
-      if (responseMessage == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
-
-    // See if there are any controls in the response.  If so, then add them to
-    // the response controls list.
-    List<Control> respControls = responseMessage.getControls();
-    if (respControls != null && ! respControls.isEmpty())
-    {
-      responseControls.addAll(respControls);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    switch (responseMessage.getProtocolOpType())
-    {
-      case OP_TYPE_BIND_RESPONSE:
-        // We'll deal with this later.
-        break;
-
-      case OP_TYPE_EXTENDED_RESPONSE:
-        ExtendedResponseProtocolOp extendedResponse =
-             responseMessage.getExtendedResponseProtocolOp();
-        String responseOID = extendedResponse.getOID();
-        if (responseOID != null &&
-            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
-              get(extendedResponse.getResultCode(),
-                  extendedResponse.getErrorMessage());
-          throw new LDAPException(extendedResponse.getResultCode(), message);
-        }
-        else
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
-          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-        }
-
-      default:
-        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp());
-        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
-    }
-
+    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
+    responseControls.addAll(responseMessage.getControls());
+    checkConnected(responseMessage);
 
     BindResponseProtocolOp bindResponse =
          responseMessage.getBindResponseProtocolOp();
@@ -2409,7 +1664,57 @@
                             message, bindResponse.getMatchedDN(), null);
   }
 
+  private void sendBindRequest(String saslMechanism, ByteSequence bindDN, ByteString saslCredentials,
+      List<Control> requestControls) throws ClientException
+  {
+    BindRequestProtocolOp bindRequest =
+        new BindRequestProtocolOp(bindDN.toByteString(), saslMechanism, saslCredentials);
+    LDAPMessage requestMessage = new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest, requestControls);
 
+    try
+    {
+      writer.writeMessage(requestMessage);
+    }
+    catch (IOException ioe)
+    {
+      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(saslMechanism, getExceptionMessage(ioe));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
+    }
+    catch (Exception e)
+    {
+      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(saslMechanism, getExceptionMessage(e));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
+    }
+  }
+
+  private LDAPMessage readBindResponse(Arg1<Object> errCannotReadBindResponse) throws ClientException
+  {
+    try
+    {
+      LDAPMessage responseMessage = reader.readMessage();
+      if (responseMessage != null)
+      {
+        return responseMessage;
+      }
+      LocalizableMessage message = ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message);
+    }
+    catch (DecodeException | LDAPException e)
+    {
+      LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(e));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
+    }
+    catch (IOException ioe)
+    {
+      LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(ioe));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
+    }
+    catch (Exception e)
+    {
+      LocalizableMessage message = errCannotReadBindResponse.get(getExceptionMessage(e));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
+    }
+  }
 
   /**
    * Retrieves the set of properties that a client may provide when performing a
@@ -2420,7 +1725,7 @@
    *          SASL EXTERNAL bind, mapped from the property names to their
    *          corresponding descriptions.
    */
-  public static LinkedHashMap<String,LocalizableMessage> getSASLExternalProperties()
+  private static LinkedHashMap<String, LocalizableMessage> getSASLExternalProperties()
   {
     // There are no properties for the SASL EXTERNAL mechanism.
     return new LinkedHashMap<>(0);
@@ -2455,7 +1760,7 @@
    * @throws  LDAPException  If the bind fails or some other server-side problem
    *                         occurs during processing.
    */
-  public String doSASLGSSAPI(ByteSequence bindDN,
+  private String doSASLGSSAPI(ByteSequence bindDN,
                      ByteSequence bindPassword,
                      Map<String,List<String>> saslProperties,
                      List<Control> requestControls,
@@ -2469,16 +1774,7 @@
     gssapiAuthID  = null;
     gssapiAuthzID = null;
     gssapiQoP     = "auth";
-
-    if (bindPassword == null)
-    {
-      gssapiAuthPW = null;
-    }
-    else
-    {
-      gssapiAuthPW = bindPassword.toString().toCharArray();
-    }
-
+    gssapiAuthPW = bindPassword != null ? bindPassword.toString().toCharArray() : null;
 
     // Evaluate the properties provided.  The authID is required.  The authzID,
     // KDC, QoP, and realm are optional.
@@ -2490,60 +1786,26 @@
               ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
     }
 
-    for (String name : saslProperties.keySet())
+    for (Entry<String, List<String>> entry : saslProperties.entrySet())
     {
+      String name = entry.getKey();
       String lowerName = toLowerCase(name);
+      List<String> values = entry.getValue();
 
       if (lowerName.equals(SASL_PROPERTY_AUTHID))
       {
-        List<String> values = saslProperties.get(name);
-        Iterator<String> iterator = values.iterator();
-        if (iterator.hasNext())
-        {
-          gssapiAuthID = iterator.next();
-
-          if (iterator.hasNext())
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
-          }
-        }
+        gssapiAuthID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
       }
       else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
       {
-        List<String> values = saslProperties.get(name);
-        Iterator<String> iterator = values.iterator();
-        if (iterator.hasNext())
-        {
-          gssapiAuthzID = iterator.next();
-
-          if (iterator.hasNext())
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                                      message);
-          }
-        }
+        gssapiAuthzID = getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED);
       }
       else if (lowerName.equals(SASL_PROPERTY_KDC))
       {
-        List<String> values = saslProperties.get(name);
-        Iterator<String> iterator = values.iterator();
-        if (iterator.hasNext())
-        {
-          kdc = iterator.next();
-
-          if (iterator.hasNext())
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_KDC_SINGLE_VALUED.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                                      message);
-          }
-        }
+        kdc = getSingleValue(values, ERR_LDAPAUTH_KDC_SINGLE_VALUED);
       }
       else if (lowerName.equals(SASL_PROPERTY_QOP))
       {
-        List<String> values = saslProperties.get(name);
         Iterator<String> iterator = values.iterator();
         if (iterator.hasNext())
         {
@@ -2580,19 +1842,7 @@
       }
       else if (lowerName.equals(SASL_PROPERTY_REALM))
       {
-        List<String> values = saslProperties.get(name);
-        Iterator<String> iterator = values.iterator();
-        if (iterator.hasNext())
-        {
-          realm = iterator.next();
-
-          if (iterator.hasNext())
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                                      message);
-          }
-        }
+        realm = getSingleValue(values, ERR_LDAPAUTH_REALM_SINGLE_VALUED);
       }
       else
       {
@@ -2645,20 +1895,17 @@
       File tempFile = File.createTempFile("login", "conf");
       configFileName = tempFile.getAbsolutePath();
       tempFile.deleteOnExit();
-      BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false));
+      try (BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false))) {
+        w.write(getClass().getName() + " {");
+        w.newLine();
 
-      w.write(getClass().getName() + " {");
-      w.newLine();
+        w.write("  com.sun.security.auth.module.Krb5LoginModule required " +
+            "client=TRUE useTicketCache=TRUE;");
+        w.newLine();
 
-      w.write("  com.sun.security.auth.module.Krb5LoginModule required " +
-              "client=TRUE useTicketCache=TRUE;");
-      w.newLine();
-
-      w.write("};");
-      w.newLine();
-
-      w.flush();
-      w.close();
+        w.write("};");
+        w.newLine();
+      }
     }
     catch (Exception e)
     {
@@ -2710,13 +1957,25 @@
     }
 
 
-    // FIXME --  Need to make sure we handle request and response controls
-    // properly, and also check for any possible message to send back to the
-    // client.
+    // FIXME -- Need to make sure we handle request and response controls properly,
+    // and also check for any possible message to send back to the client.
     return null;
   }
 
-
+  private String getSingleValue(List<String> values, Arg0 singleValuedErrMsg) throws ClientException
+  {
+    Iterator<String> it = values.iterator();
+    if (it.hasNext())
+    {
+      String result = it.next();
+      if (it.hasNext())
+      {
+        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, singleValuedErrMsg.get());
+      }
+      return result;
+    }
+    return null;
+  }
 
   /**
    * Retrieves the set of properties that a client may provide when performing a
@@ -2727,7 +1986,7 @@
    *          SASL EXTERNAL bind, mapped from the property names to their
    *          corresponding descriptions.
    */
-  public static LinkedHashMap<String,LocalizableMessage> getSASLGSSAPIProperties()
+  private static LinkedHashMap<String, LocalizableMessage> getSASLGSSAPIProperties()
   {
     LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(4);
 
@@ -2791,29 +2050,19 @@
               ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
     }
 
-    for (String name : saslProperties.keySet())
+    for (Entry<String, List<String>> entry : saslProperties.entrySet())
     {
+      String name = entry.getKey();
+      List<String> values = entry.getValue();
       String lowerName = toLowerCase(name);
 
       if (lowerName.equals(SASL_PROPERTY_AUTHID))
       {
-        authID = getAuthID(saslProperties, authID, name);
+        authID = getSingleValue(values, ERR_LDAPAUTH_AUTHID_SINGLE_VALUED);
       }
       else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
       {
-        List<String> values = saslProperties.get(name);
-        Iterator<String> iterator = values.iterator();
-        if (iterator.hasNext())
-        {
-          authzID = iterator.next();
-
-          if (iterator.hasNext())
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
-                                      message);
-          }
-        }
+        authzID = getSingleValue(values, ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED);
       }
       else
       {
@@ -2841,116 +2090,17 @@
         bindPassword = ByteString.empty();
     }
 
-
     // Construct the bind request and send it to the server.
-    StringBuilder credBuffer = new StringBuilder();
-    if (authzID != null)
-    {
-      credBuffer.append(authzID);
-    }
-    credBuffer.append('\u0000');
-    credBuffer.append(authID);
-    credBuffer.append('\u0000');
-    credBuffer.append(bindPassword.toString());
+    String saslCredentials = (authzID != null ? authzID : "") + '\u0000' + authID + '\u0000' + bindPassword;
+    sendBindRequest(SASL_MECHANISM_PLAIN, bindDN, ByteString.valueOfUtf8(saslCredentials), requestControls);
 
-    ByteString saslCredentials =
-        ByteString.valueOfUtf8(credBuffer.toString());
-    BindRequestProtocolOp bindRequest =
-         new BindRequestProtocolOp(bindDN.toByteString(), SASL_MECHANISM_PLAIN,
-                                saslCredentials);
-    LDAPMessage requestMessage =
-         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
-                         requestControls);
-
-    try
-    {
-      writer.writeMessage(requestMessage);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-          SASL_MECHANISM_PLAIN, getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-          SASL_MECHANISM_PLAIN, getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
-                                message, e);
-    }
-
-
-    // Read the response from the server.
-    LDAPMessage responseMessage;
-    try
-    {
-      responseMessage = reader.readMessage();
-      if (responseMessage == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
-
-    // See if there are any controls in the response.  If so, then add them to
-    // the response controls list.
-    List<Control> respControls = responseMessage.getControls();
-    if (respControls != null && !respControls.isEmpty())
-    {
-      responseControls.addAll(respControls);
-    }
-
-
-    // Look at the protocol op from the response.  If it's a bind response, then
-    // continue.  If it's an extended response, then it could be a notice of
-    // disconnection so check for that.  Otherwise, generate an error.
-    generateError(responseMessage);
-
-
-    BindResponseProtocolOp bindResponse =
-         responseMessage.getBindResponseProtocolOp();
-    int resultCode = bindResponse.getResultCode();
-    if (resultCode == ReturnCode.SUCCESS.get())
-    {
-      // FIXME -- Need to look for things like password expiration warning,
-      // reset notice, etc.
-      return null;
-    }
-
-    // FIXME -- Add support for referrals.
-
-    LocalizableMessage message = ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_PLAIN);
-    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
-                            message, bindResponse.getMatchedDN(), null);
+    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
+    responseControls.addAll(responseMessage.getControls());
+    checkConnected(responseMessage);
+    checkSuccessfulBind(responseMessage, SASL_MECHANISM_PLAIN);
+    return null;
   }
 
-
-
   /**
    * Retrieves the set of properties that a client may provide when performing a
    * SASL PLAIN bind, mapped from the property names to their corresponding
@@ -2960,7 +2110,7 @@
    *          SASL PLAIN bind, mapped from the property names to their
    *          corresponding descriptions.
    */
-  public static LinkedHashMap<String,LocalizableMessage> getSASLPlainProperties()
+  private static LinkedHashMap<String, LocalizableMessage> getSASLPlainProperties()
   {
     LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(2);
 
@@ -2988,8 +2138,7 @@
    *                         processing.
    */
   @Override
-  public Object run()
-         throws ClientException, LDAPException
+  public Object run() throws ClientException, LDAPException
   {
     if (saslMechanism == null)
     {
@@ -2999,290 +2148,151 @@
     }
     else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
     {
-      // Create the property map that will be used by the internal SASL handler.
-      HashMap<String,String> saslProperties = new HashMap<>();
-      saslProperties.put(Sasl.QOP, gssapiQoP);
-      saslProperties.put(Sasl.SERVER_AUTH, "true");
-
-
-      // Create the SASL client that we will use to actually perform the
-      // authentication.
-      SaslClient saslClient;
-      try
-      {
-        saslClient =
-             Sasl.createSaslClient(new String[] { SASL_MECHANISM_GSSAPI },
-                                   gssapiAuthzID, "ldap", hostName,
-                                   saslProperties, this);
-      }
-      catch (Exception e)
-      {
-        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get(
-            getExceptionMessage(e));
-        throw new ClientException(
-                ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-      }
-
-
-      // Get the SASL credentials to include in the initial bind request.
-      ByteString saslCredentials;
-      if (saslClient.hasInitialResponse())
-      {
-        try
-        {
-          byte[] credBytes = saslClient.evaluateChallenge(new byte[0]);
-          saslCredentials = ByteString.wrap(credBytes);
-        }
-        catch (Exception e)
-        {
-          LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_INITIAL_CHALLENGE.
-              get(getExceptionMessage(e));
-          throw new ClientException(
-                  ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
-                                    message, e);
-        }
-      }
-      else
-      {
-        saslCredentials = null;
-      }
-
-
-      BindRequestProtocolOp bindRequest =
-           new BindRequestProtocolOp(gssapiBindDN.toByteString(),
-               SASL_MECHANISM_GSSAPI, saslCredentials);
-      // FIXME -- Add controls here?
-      LDAPMessage requestMessage =
-           new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
-
-      try
-      {
-        writer.writeMessage(requestMessage);
-      }
-      catch (IOException ioe)
-      {
-        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-            SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe));
-        throw new ClientException(
-                ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-      }
-      catch (Exception e)
-      {
-        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-            SASL_MECHANISM_GSSAPI, getExceptionMessage(e));
-        throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
-                                  message, e);
-      }
-
-
-      // Read the response from the server.
-      LDAPMessage responseMessage;
-      try
-      {
-        responseMessage = reader.readMessage();
-        if (responseMessage == null)
-        {
-          LocalizableMessage message =
-              ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-          throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                    message);
-        }
-      }
-      catch (DecodeException | LDAPException e)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-        throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-      }
-      catch (IOException ioe)
-      {
-        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
-            getExceptionMessage(ioe));
-        throw new ClientException(
-                ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-      }
-      catch (Exception e)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-        throw new ClientException(
-                ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-      }
-
-
-      // FIXME -- Handle response controls.
-
-
-      // Look at the protocol op from the response.  If it's a bind response,
-      // then continue.  If it's an extended response, then it could be a notice
-      // of disconnection so check for that.  Otherwise, generate an error.
-      generateError(responseMessage);
-
-
-      while (true)
-      {
-        BindResponseProtocolOp bindResponse =
-             responseMessage.getBindResponseProtocolOp();
-        int resultCode = bindResponse.getResultCode();
-        if (resultCode == ReturnCode.SUCCESS.get())
-        {
-          // We should be done after this, but we still need to look for and
-          // handle the server SASL credentials.
-          ByteString serverSASLCredentials =
-               bindResponse.getServerSASLCredentials();
-          if (serverSASLCredentials != null)
-          {
-            try
-            {
-              saslClient.evaluateChallenge(serverSASLCredentials.toByteArray());
-            }
-            catch (Exception e)
-            {
-              LocalizableMessage message =
-                  ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.
-                    get(getExceptionMessage(e));
-              throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
-                                        message, e);
-            }
-          }
-
-
-          // Just to be sure, check that the login really is complete.
-          if (! saslClient.isComplete())
-          {
-            LocalizableMessage message =
-                ERR_LDAPAUTH_GSSAPI_UNEXPECTED_SUCCESS_RESPONSE.get();
-            throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
-                                      message);
-          }
-
-          break;
-        }
-        else if (resultCode == ReturnCode.SASL_BIND_IN_PROGRESS.get())
-        {
-          // Read the response and process the server SASL credentials.
-          ByteString serverSASLCredentials =
-               bindResponse.getServerSASLCredentials();
-          byte[] credBytes;
-          try
-          {
-            if (serverSASLCredentials == null)
-            {
-              credBytes = saslClient.evaluateChallenge(new byte[0]);
-            }
-            else
-            {
-              credBytes = saslClient.evaluateChallenge(
-                  serverSASLCredentials.toByteArray());
-            }
-          }
-          catch (Exception e)
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.
-                get(getExceptionMessage(e));
-            throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
-                                      message, e);
-          }
-
-
-          // Send the next bind in the sequence to the server.
-          bindRequest =
-               new BindRequestProtocolOp(gssapiBindDN.toByteString(),
-                   SASL_MECHANISM_GSSAPI, ByteString.wrap(credBytes));
-          // FIXME -- Add controls here?
-          requestMessage =
-               new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
-
-
-          try
-          {
-            writer.writeMessage(requestMessage);
-          }
-          catch (IOException ioe)
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-                SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe));
-            throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                      message, ioe);
-          }
-          catch (Exception e)
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
-                SASL_MECHANISM_GSSAPI, getExceptionMessage(e));
-            throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
-                                      message, e);
-          }
-
-
-          // Read the response from the server.
-          try
-          {
-            responseMessage = reader.readMessage();
-            if (responseMessage == null)
-            {
-              LocalizableMessage message =
-                  ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-              throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                        message);
-            }
-          }
-          catch (DecodeException | LDAPException e)
-          {
-            LocalizableMessage message =
-                ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
-            throw new ClientException(
-                ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-          }
-          catch (IOException ioe)
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
-                getExceptionMessage(ioe));
-            throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                      message, ioe);
-          }
-          catch (Exception e)
-          {
-            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
-                getExceptionMessage(e));
-            throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
-                                      message, e);
-          }
-
-
-          // FIXME -- Handle response controls.
-
-
-          // Look at the protocol op from the response.  If it's a bind
-          // response, then continue.  If it's an extended response, then it
-          // could be a notice of disconnection so check for that.  Otherwise,
-          // generate an error.
-          generateError(responseMessage);
-        }
-        else
-        {
-          // This is an error.
-          LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get();
-          throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
-                                  message, bindResponse.getMatchedDN(),
-                                  null);
-        }
-      }
+      doSASLGSSAPI2();
+      return null;
     }
     else
     {
       LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RUN_INVOCATION.get(
           saslMechanism, getBacktrace());
+      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
+    }
+  }
+
+  private void doSASLGSSAPI2() throws ClientException, LDAPException
+  {
+    // Create the property map that will be used by the internal SASL handler.
+    Map<String, String> saslProperties = new HashMap<>();
+    saslProperties.put(Sasl.QOP, gssapiQoP);
+    saslProperties.put(Sasl.SERVER_AUTH, "true");
+
+
+    // Create the SASL client that we will use to actually perform the
+    // authentication.
+    SaslClient saslClient;
+    try
+    {
+      saslClient =
+           Sasl.createSaslClient(new String[] { SASL_MECHANISM_GSSAPI },
+                                 gssapiAuthzID, "ldap", hostName,
+                                 saslProperties, this);
+    }
+    catch (Exception e)
+    {
+      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get(
+          getExceptionMessage(e));
       throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
+              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
     }
 
+    // FIXME -- Add controls here?
+    ByteString saslCredentials = getSaslCredentialsForInitialBind(saslClient);
+    sendBindRequest(SASL_MECHANISM_GSSAPI, gssapiBindDN, saslCredentials, null);
 
-    // FIXME -- Need to look for things like password expiration warning, reset
-    // notice, etc.
+    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
+    // FIXME -- Handle response controls.
+    checkConnected(responseMessage);
+
+    while (true)
+    {
+      BindResponseProtocolOp bindResponse =
+           responseMessage.getBindResponseProtocolOp();
+      int resultCode = bindResponse.getResultCode();
+      if (resultCode == ReturnCode.SUCCESS.get())
+      {
+        evaluateGSSAPIChallenge(saslClient, bindResponse);
+        break;
+      }
+      else if (resultCode == ReturnCode.SASL_BIND_IN_PROGRESS.get())
+      {
+        // FIXME -- Add controls here?
+        ByteString credBytes = evaluateSaslChallenge(saslClient, bindResponse);
+        sendBindRequest(SASL_MECHANISM_GSSAPI, gssapiBindDN, credBytes, null);
+
+        responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE);
+        // FIXME -- Handle response controls.
+        checkConnected(responseMessage);
+      }
+      else
+      {
+        // This is an error.
+        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get();
+        throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
+                                message, bindResponse.getMatchedDN(),
+                                null);
+      }
+    }
+    // FIXME -- Need to look for things like password expiration warning, reset notice, etc.
+  }
+
+
+  private void evaluateGSSAPIChallenge(SaslClient saslClient, BindResponseProtocolOp bindResponse)
+      throws ClientException
+  {
+    // We should be done after this, but we still need to look for and
+    // handle the server SASL credentials.
+    ByteString serverSASLCredentials = bindResponse.getServerSASLCredentials();
+    if (serverSASLCredentials != null)
+    {
+      try
+      {
+        saslClient.evaluateChallenge(serverSASLCredentials.toByteArray());
+      }
+      catch (Exception e)
+      {
+        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.get(getExceptionMessage(e));
+        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
+      }
+    }
+
+    // Just to be sure, check that the login really is complete.
+    if (!saslClient.isComplete())
+    {
+      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_UNEXPECTED_SUCCESS_RESPONSE.get();
+      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
+    }
+  }
+
+  private ByteString evaluateSaslChallenge(SaslClient saslClient, BindResponseProtocolOp bindResponse)
+      throws ClientException
+  {
+    try
+    {
+      ByteString saslCredentials = bindResponse.getServerSASLCredentials();
+      byte[] bs = saslCredentials != null ? saslCredentials.toByteArray() : new byte[0];
+      return ByteString.wrap(saslClient.evaluateChallenge(bs));
+    }
+    catch (Exception e)
+    {
+      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.get(getExceptionMessage(e));
+      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
+    }
+  }
+
+  private ByteString getSaslCredentialsForInitialBind(SaslClient saslClient) throws ClientException
+  {
+    if (saslClient.hasInitialResponse())
+    {
+      try
+      {
+        return ByteString.wrap(saslClient.evaluateChallenge(new byte[0]));
+      }
+      catch (Exception e)
+      {
+        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_INITIAL_CHALLENGE.get(getExceptionMessage(e));
+        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
+      }
+    }
     return null;
   }
 
-  private void generateError(LDAPMessage responseMessage) throws LDAPException, ClientException
+  /**
+   * Look at the protocol op from the response.
+   * If it's a bind response, then continue.
+   * If it's an extended response, then check it is not a notice of disconnection.
+   * Otherwise, generate an error.
+   */
+  private void checkConnected(LDAPMessage responseMessage) throws LDAPException, ClientException
   {
     switch (responseMessage.getProtocolOpType())
     {
@@ -3417,40 +2427,7 @@
     }
 
 
-    // Read the response from the server.
-    LDAPMessage responseMessage;
-    try
-    {
-      responseMessage = reader.readMessage();
-      if (responseMessage == null)
-      {
-        LocalizableMessage message =
-            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
-        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
-                                  message);
-      }
-    }
-    catch (DecodeException | LDAPException e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
-    }
-    catch (IOException ioe)
-    {
-      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(
-          getExceptionMessage(ioe));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
-    }
-    catch (Exception e)
-    {
-      LocalizableMessage message =
-          ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e));
-      throw new ClientException(
-              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
-    }
-
+    LDAPMessage responseMessage = readBindResponse(ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE);
 
     // If the protocol op isn't an extended response, then that's a problem.
     if (responseMessage.getProtocolOpType() != OP_TYPE_EXTENDED_RESPONSE)
@@ -3493,13 +2470,10 @@
       return null;
     }
 
-    String valueString = authzID.toString();
-    if (valueString == null || valueString.length() == 0 ||
-        valueString.equalsIgnoreCase("dn:"))
+    if (!"dn:".equalsIgnoreCase(authzID.toString()))
     {
-      return null;
+      return authzID;
     }
-
-    return authzID;
+    return null;
   }
 }

--
Gitblit v1.10.0