/*
|
* CDDL HEADER START
|
*
|
* The contents of this file are subject to the terms of the
|
* Common Development and Distribution License, Version 1.0 only
|
* (the "License"). You may not use this file except in compliance
|
* with the License.
|
*
|
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* See the License for the specific language governing permissions
|
* and limitations under the License.
|
*
|
* When distributing Covered Code, include this CDDL HEADER in each
|
* file and include the License file at legal-notices/CDDLv1_0.txt.
|
* If applicable, add the following below this CDDL HEADER, with the
|
* fields enclosed by brackets "[]" replaced with your own identifying
|
* information:
|
* Portions Copyright [yyyy] [name of copyright owner]
|
*
|
* CDDL HEADER END
|
*
|
*
|
* Copyright 2006-2009 Sun Microsystems, Inc.
|
* Portions Copyright 2012-2014 ForgeRock AS
|
*/
|
package org.opends.server.tools;
|
|
import java.io.BufferedWriter;
|
import java.io.File;
|
import java.io.FileWriter;
|
import java.io.IOException;
|
import java.io.UnsupportedEncodingException;
|
import java.security.MessageDigest;
|
import java.security.PrivilegedExceptionAction;
|
import java.security.SecureRandom;
|
import java.util.Arrays;
|
import java.util.HashMap;
|
import java.util.Iterator;
|
import java.util.LinkedHashMap;
|
import java.util.LinkedList;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.StringTokenizer;
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import javax.security.auth.Subject;
|
import javax.security.auth.callback.Callback;
|
import javax.security.auth.callback.CallbackHandler;
|
import javax.security.auth.callback.NameCallback;
|
import javax.security.auth.callback.PasswordCallback;
|
import javax.security.auth.callback.UnsupportedCallbackException;
|
import javax.security.auth.login.LoginContext;
|
import javax.security.sasl.Sasl;
|
import javax.security.sasl.SaslClient;
|
|
import com.forgerock.opendj.cli.ClientException;
|
import com.forgerock.opendj.cli.ReturnCode;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.opendj.ldap.ByteSequence;
|
import org.forgerock.opendj.ldap.ByteString;
|
import org.forgerock.opendj.ldap.DecodeException;
|
import org.opends.server.protocols.ldap.BindRequestProtocolOp;
|
import org.opends.server.protocols.ldap.BindResponseProtocolOp;
|
import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp;
|
import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp;
|
import org.opends.server.protocols.ldap.LDAPMessage;
|
import org.opends.server.types.LDAPException;
|
import org.opends.server.types.Control;
|
import org.opends.server.util.Base64;
|
import org.opends.server.util.PasswordReader;
|
|
import static org.opends.messages.ToolMessages.*;
|
import static org.opends.server.protocols.ldap.LDAPConstants.*;
|
import static org.opends.server.tools.ToolConstants.*;
|
import static org.opends.server.util.ServerConstants.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
|
|
/**
|
* This class provides a generic interface that LDAP clients can use to perform
|
* various kinds of authentication to the Directory Server. This handles both
|
* simple authentication as well as several SASL mechanisms including:
|
* <UL>
|
* <LI>ANONYMOUS</LI>
|
* <LI>CRAM-MD5</LI>
|
* <LI>DIGEST-MD5</LI>
|
* <LI>EXTERNAL</LI>
|
* <LI>GSSAPI</LI>
|
* <LI>PLAIN</LI>
|
* </UL>
|
* <BR><BR>
|
* Note that this implementation is not thread safe, so if the same
|
* <CODE>AuthenticationHandler</CODE> object is to be used concurrently by
|
* multiple threads, it must be externally synchronized.
|
*/
|
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.
|
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 authentication ID for GSSAPI authentication.
|
private String gssapiAuthID;
|
|
// The authorization ID for GSSAPI authentication.
|
private String gssapiAuthzID;
|
|
// The quality of protection for GSSAPI authentication.
|
private String gssapiQoP;
|
|
// The host name used to connect to the remote system.
|
private final String hostName;
|
|
// The SASL mechanism that will be used for callback authentication.
|
private String saslMechanism;
|
|
|
|
/**
|
* Creates a new instance of this authentication handler. All initialization
|
* will be done lazily to avoid unnecessary performance hits, particularly
|
* for cases in which simple authentication will be used as it does not
|
* require any particularly expensive processing.
|
*
|
* @param reader The LDAP reader that will be used to read data from
|
* the server.
|
* @param writer The LDAP writer that will be used to send data to
|
* the server.
|
* @param hostName The host name used to connect to the remote system
|
* (fully-qualified if possible).
|
* @param nextMessageID The atomic integer that will be used to obtain
|
* message IDs for request messages.
|
*/
|
public LDAPAuthenticationHandler(LDAPReader reader, LDAPWriter writer,
|
String hostName, AtomicInteger nextMessageID)
|
{
|
this.reader = reader;
|
this.writer = writer;
|
this.hostName = hostName;
|
this.nextMessageID = nextMessageID;
|
|
md5Digest = null;
|
secureRandom = null;
|
iPad = null;
|
oPad = null;
|
}
|
|
|
|
/**
|
* Retrieves a list of the SASL mechanisms that are supported by this client
|
* library.
|
*
|
* @return A list of the SASL mechanisms that are supported by this client
|
* library.
|
*/
|
public static String[] getSupportedSASLMechanisms()
|
{
|
return new String[]
|
{
|
SASL_MECHANISM_ANONYMOUS,
|
SASL_MECHANISM_CRAM_MD5,
|
SASL_MECHANISM_DIGEST_MD5,
|
SASL_MECHANISM_EXTERNAL,
|
SASL_MECHANISM_GSSAPI,
|
SASL_MECHANISM_PLAIN
|
};
|
}
|
|
|
|
/**
|
* Retrieves a list of the SASL properties that may be provided for the
|
* specified SASL mechanism, mapped from the property names to their
|
* corresponding descriptions.
|
*
|
* @param mechanism The name of the SASL mechanism for which to obtain the
|
* list of supported properties.
|
*
|
* @return A list of the SASL properties that may be provided for the
|
* specified SASL mechanism, mapped from the property names to their
|
* corresponding descriptions.
|
*/
|
public static LinkedHashMap<String,LocalizableMessage> getSASLProperties(
|
String mechanism)
|
{
|
String upperName = toUpperCase(mechanism);
|
if (upperName.equals(SASL_MECHANISM_ANONYMOUS))
|
{
|
return getSASLAnonymousProperties();
|
}
|
else if (upperName.equals(SASL_MECHANISM_CRAM_MD5))
|
{
|
return getSASLCRAMMD5Properties();
|
}
|
else if (upperName.equals(SASL_MECHANISM_DIGEST_MD5))
|
{
|
return getSASLDigestMD5Properties();
|
}
|
else if (upperName.equals(SASL_MECHANISM_EXTERNAL))
|
{
|
return getSASLExternalProperties();
|
}
|
else if (upperName.equals(SASL_MECHANISM_GSSAPI))
|
{
|
return getSASLGSSAPIProperties();
|
}
|
else if (upperName.equals(SASL_MECHANISM_PLAIN))
|
{
|
return getSASLPlainProperties();
|
}
|
else
|
{
|
// This is an unsupported mechanism.
|
return null;
|
}
|
}
|
|
|
|
/**
|
* Processes a bind using simple authentication with the provided information.
|
* If the bind fails, then an exception will be thrown with information about
|
* the reason for the failure. If the bind is successful but there may be
|
* some special information that the client should be given, then it will be
|
* returned as a String.
|
*
|
* @param ldapVersion The LDAP protocol version to use for the bind
|
* request.
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if it is to be an anonymous
|
* bind.
|
* @param bindPassword The password to use to bind to the Directory
|
* Server, or <CODE>null</CODE> if it is to be an
|
* anonymous bind.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSimpleBind(int ldapVersion, ByteSequence bindDN,
|
ByteSequence bindPassword,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
//Password is empty, set it to ByteString.empty.
|
if (bindPassword == null)
|
{
|
bindPassword = ByteString.empty();
|
}
|
|
|
// Make sure that critical elements aren't null.
|
if (bindDN == null)
|
{
|
bindDN = ByteString.empty();
|
}
|
|
|
// Create the bind request and send it to the server.
|
BindRequestProtocolOp bindRequest =
|
new BindRequestProtocolOp(bindDN.toByteString(), ldapVersion,
|
bindPassword.toByteString());
|
LDAPMessage bindRequestMessage =
|
new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
|
requestControls);
|
|
try
|
{
|
writer.writeMessage(bindRequestMessage);
|
}
|
catch (IOException 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));
|
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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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);
|
}
|
|
|
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);
|
}
|
|
|
|
/**
|
* Processes a SASL bind using the provided information. If the bind fails,
|
* then an exception will be thrown with information about the reason for the
|
* failure. If the bind is successful but there may be some special
|
* information that the client should be given, then it will be returned as a
|
* String.
|
*
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if the authentication identity
|
* is to be set through some other means.
|
* @param bindPassword The password to use to bind to the Directory
|
* Server, or <CODE>null</CODE> if this is not a
|
* password-based SASL mechanism.
|
* @param mechanism The name of the SASL mechanism to use to
|
* authenticate to the Directory Server.
|
* @param saslProperties A set of additional properties that may be needed
|
* to process the SASL bind.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSASLBind(ByteSequence bindDN, ByteSequence bindPassword,
|
String mechanism,
|
Map<String,List<String>> saslProperties,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
// Make sure that critical elements aren't null.
|
if (bindDN == null)
|
{
|
bindDN = ByteString.empty();
|
}
|
|
if ((mechanism == null) || (mechanism.length() == 0))
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_NO_SASL_MECHANISM.get();
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
|
// Look at the mechanism name and call the appropriate method to process
|
// the request.
|
saslMechanism = toUpperCase(mechanism);
|
if (saslMechanism.equals(SASL_MECHANISM_ANONYMOUS))
|
{
|
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
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mechanism);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_AUTH_UNKNOWN, message);
|
}
|
}
|
|
|
|
/**
|
* Processes a SASL ANONYMOUS bind with the provided information.
|
*
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if the authentication identity
|
* is to be set through some other means.
|
* @param saslProperties A set of additional properties that may be needed
|
* to process the SASL bind.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSASLAnonymous(ByteSequence bindDN,
|
Map<String,List<String>> saslProperties,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
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())
|
{
|
// This is fine because there are no required properties for this
|
// mechanism.
|
}
|
else
|
{
|
Iterator<String> propertyNames = saslProperties.keySet().iterator();
|
while (propertyNames.hasNext())
|
{
|
String name = propertyNames.next();
|
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);
|
}
|
}
|
}
|
else
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
|
name, SASL_MECHANISM_ANONYMOUS);
|
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.valueOf(trace);
|
}
|
|
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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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);
|
}
|
|
|
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);
|
}
|
|
|
|
/**
|
* Retrieves the set of properties that a client may provide when performing a
|
* SASL ANONYMOUS bind, mapped from the property names to their corresponding
|
* descriptions.
|
*
|
* @return The set of properties that a client may provide when performing a
|
* SASL ANONYMOUS bind, mapped from the property names to their
|
* corresponding descriptions.
|
*/
|
public static LinkedHashMap<String, LocalizableMessage> getSASLAnonymousProperties()
|
{
|
LinkedHashMap<String,LocalizableMessage> properties =
|
new LinkedHashMap<String,LocalizableMessage>(1);
|
|
properties.put(SASL_PROPERTY_TRACE,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_TRACE.get());
|
|
return properties;
|
}
|
|
|
|
/**
|
* Processes a SASL CRAM-MD5 bind with the provided information.
|
*
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if the authentication identity
|
* is to be set through some other means.
|
* @param bindPassword The password to use to bind to the Directory
|
* Server.
|
* @param saslProperties A set of additional properties that may be needed
|
* to process the SASL bind.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSASLCRAMMD5(ByteSequence bindDN,
|
ByteSequence bindPassword,
|
Map<String,List<String>> saslProperties,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
String authID = null;
|
|
|
// Evaluate the properties provided. The authID is required, no other
|
// properties are allowed.
|
if ((saslProperties == null) || saslProperties.isEmpty())
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_CRAM_MD5);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
Iterator<String> propertyNames = saslProperties.keySet().iterator();
|
while (propertyNames.hasNext())
|
{
|
String name = propertyNames.next();
|
String lowerName = toLowerCase(name);
|
|
if (lowerName.equals(SASL_PROPERTY_AUTHID))
|
{
|
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);
|
}
|
}
|
}
|
else
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
|
name, SASL_MECHANISM_CRAM_MD5);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
}
|
|
|
// Make sure that the authID was provided.
|
if ((authID == null) || (authID.length() == 0))
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_CRAM_MD5);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
|
// Set password to ByteString.empty if the password is null.
|
if (bindPassword == null)
|
{
|
bindPassword = ByteString.empty();
|
}
|
|
|
// 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);
|
|
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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
|
SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
|
SASL_MECHANISM_CRAM_MD5, getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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.
|
BindResponseProtocolOp bindResponse1 =
|
responseMessage1.getBindResponseProtocolOp();
|
int resultCode1 = bindResponse1.getResultCode();
|
if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get())
|
{
|
LocalizableMessage errorMessage = bindResponse1.getErrorMessage();
|
if (errorMessage == null)
|
{
|
errorMessage = LocalizableMessage.EMPTY;
|
}
|
|
LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
|
get(SASL_MECHANISM_CRAM_MD5, resultCode1,
|
ReturnCode.get(resultCode1), errorMessage);
|
throw new LDAPException(resultCode1, errorMessage, message,
|
bindResponse1.getMatchedDN(), null);
|
}
|
|
|
// Make sure that the bind response contains SASL credentials with the
|
// challenge to use for the next stage of the bind.
|
ByteString serverChallenge = bindResponse1.getServerSASLCredentials();
|
if (serverChallenge == null)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_NO_CRAMMD5_SERVER_CREDENTIALS.get();
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
|
// 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));
|
|
|
// Create and send the second bind request to the server.
|
BindRequestProtocolOp bindRequest2 =
|
new BindRequestProtocolOp(bindDN.toByteString(),
|
SASL_MECHANISM_CRAM_MD5, ByteString.valueOf(buffer.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_CRAM_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_CRAM_MD5, getExceptionMessage(e));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
|
SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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);
|
}
|
catch (LDAPException le)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
|
SASL_MECHANISM_CRAM_MD5, getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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);
|
}
|
|
|
// 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);
|
}
|
|
|
|
/**
|
* Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication
|
* with the given information.
|
*
|
* @param password The clear-text password to use when generating the
|
* digest.
|
* @param challenge The server-supplied challenge to use when generating the
|
* digest.
|
*
|
* @return The generated HMAC-MD5 digest for CRAM-MD5 authentication.
|
*
|
* @throws ClientException If a problem occurs while attempting to perform
|
* the necessary initialization.
|
*/
|
private String generateCRAMMD5Digest(ByteSequence password,
|
ByteSequence challenge)
|
throws ClientException
|
{
|
// Perform the necessary initialization if it hasn't been done yet.
|
if (md5Digest == null)
|
{
|
try
|
{
|
md5Digest = MessageDigest.getInstance("MD5");
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
|
getExceptionMessage(e));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
|
message, e);
|
}
|
}
|
|
if (iPad == null)
|
{
|
iPad = new byte[HMAC_MD5_BLOCK_LENGTH];
|
oPad = new byte[HMAC_MD5_BLOCK_LENGTH];
|
Arrays.fill(iPad, CRAMMD5_IPAD_BYTE);
|
Arrays.fill(oPad, CRAMMD5_OPAD_BYTE);
|
}
|
|
|
// Get the byte arrays backing the password and challenge.
|
byte[] p = password.toByteArray();
|
byte[] c = challenge.toByteArray();
|
|
|
// If the password is longer than the HMAC-MD5 block length, then use an
|
// MD5 digest of the password rather than the password itself.
|
if (password.length() > HMAC_MD5_BLOCK_LENGTH)
|
{
|
p = md5Digest.digest(p);
|
}
|
|
|
// Create byte arrays with data needed for the hash generation.
|
byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length];
|
System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH);
|
System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length);
|
|
byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH];
|
System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH);
|
|
|
// Iterate through the bytes in the key and XOR them with the iPad and
|
// oPad as appropriate.
|
for (int i=0; i < p.length; i++)
|
{
|
iPadAndData[i] ^= p[i];
|
oPadAndHash[i] ^= p[i];
|
}
|
|
|
// Copy an MD5 digest of the iPad-XORed key and the data into the array to
|
// be hashed.
|
System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash,
|
HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH);
|
|
|
// Calculate an MD5 digest of the resulting array and get the corresponding
|
// hex string representation.
|
byte[] digestBytes = md5Digest.digest(oPadAndHash);
|
|
StringBuilder hexDigest = new StringBuilder(2*digestBytes.length);
|
for (byte b : digestBytes)
|
{
|
hexDigest.append(byteToLowerHex(b));
|
}
|
|
return hexDigest.toString();
|
}
|
|
|
|
/**
|
* Retrieves the set of properties that a client may provide when performing a
|
* SASL CRAM-MD5 bind, mapped from the property names to their corresponding
|
* descriptions.
|
*
|
* @return The set of properties that a client may provide when performing a
|
* SASL CRAM-MD5 bind, mapped from the property names to their
|
* corresponding descriptions.
|
*/
|
public static LinkedHashMap<String,LocalizableMessage> getSASLCRAMMD5Properties()
|
{
|
LinkedHashMap<String,LocalizableMessage> properties =
|
new LinkedHashMap<String,LocalizableMessage>(1);
|
|
properties.put(SASL_PROPERTY_AUTHID,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
|
|
return properties;
|
}
|
|
|
|
/**
|
* Processes a SASL DIGEST-MD5 bind with the provided information.
|
*
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if the authentication identity
|
* is to be set through some other means.
|
* @param bindPassword The password to use to bind to the Directory
|
* Server.
|
* @param saslProperties A set of additional properties that may be needed
|
* to process the SASL bind.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSASLDigestMD5(ByteSequence bindDN,
|
ByteSequence bindPassword,
|
Map<String,List<String>> saslProperties,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
String authID = null;
|
String realm = null;
|
String qop = "auth";
|
String digestURI = "ldap/" + hostName;
|
String authzID = null;
|
boolean realmSetFromProperty = false;
|
|
|
// Evaluate the properties provided. The authID is required. The realm,
|
// QoP, digest URI, and authzID are optional.
|
if ((saslProperties == null) || saslProperties.isEmpty())
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_DIGEST_MD5);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
|
Iterator<String> propertyNames = saslProperties.keySet().iterator();
|
while (propertyNames.hasNext())
|
{
|
String name = propertyNames.next();
|
String lowerName = toLowerCase(name);
|
|
if (lowerName.equals(SASL_PROPERTY_AUTHID))
|
{
|
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);
|
}
|
}
|
}
|
else if (lowerName.equals(SASL_PROPERTY_REALM))
|
{
|
List<String> values = saslProperties.get(name);
|
Iterator<String> iterator = values.iterator();
|
if (iterator.hasNext())
|
{
|
realm = iterator.next();
|
realmSetFromProperty = true;
|
|
if (iterator.hasNext())
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get();
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
}
|
}
|
else if (lowerName.equals(SASL_PROPERTY_QOP))
|
{
|
List<String> values = saslProperties.get(name);
|
Iterator<String> iterator = values.iterator();
|
if (iterator.hasNext())
|
{
|
qop = toLowerCase(iterator.next());
|
|
if (iterator.hasNext())
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
|
if (qop.equals("auth"))
|
{
|
// This is always fine.
|
}
|
else if (qop.equals("auth-int") || qop.equals("auth-conf"))
|
{
|
// FIXME -- Add support for integrity and confidentiality.
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(qop);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
else
|
{
|
// This is an illegal value.
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_QOP.get(qop);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
}
|
}
|
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);
|
}
|
}
|
}
|
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);
|
}
|
}
|
}
|
else
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
|
name, SASL_MECHANISM_DIGEST_MD5);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
}
|
|
|
// Make sure that the authID was provided.
|
if ((authID == null) || (authID.length() == 0))
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_DIGEST_MD5);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
|
|
// Set password to ByteString.empty if the password is null.
|
if (bindPassword == null)
|
{
|
bindPassword = ByteString.empty();
|
}
|
|
|
// 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);
|
|
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);
|
}
|
|
|
// 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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
|
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
|
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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.
|
BindResponseProtocolOp bindResponse1 =
|
responseMessage1.getBindResponseProtocolOp();
|
int resultCode1 = bindResponse1.getResultCode();
|
if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get())
|
{
|
LocalizableMessage errorMessage = bindResponse1.getErrorMessage();
|
if (errorMessage == null)
|
{
|
errorMessage = LocalizableMessage.EMPTY;
|
}
|
|
LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
|
get(SASL_MECHANISM_DIGEST_MD5, resultCode1,
|
ReturnCode.get(resultCode1), errorMessage);
|
throw new LDAPException(resultCode1, errorMessage, message,
|
bindResponse1.getMatchedDN(), null);
|
}
|
|
|
// Make sure that the bind response contains SASL credentials with the
|
// information to use for the next stage of the bind.
|
ByteString serverCredentials =
|
bindResponse1.getServerSASLCredentials();
|
if (serverCredentials == null)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_NO_DIGESTMD5_SERVER_CREDENTIALS.get();
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
|
// Parse the server SASL credentials to get the necessary information. In
|
// particular, look at the realm, the nonce, the QoP modes, and the charset.
|
// We'll only care about the realm if none was provided in the SASL
|
// properties and only one was provided in the server SASL credentials.
|
String credString = serverCredentials.toString();
|
String lowerCreds = toLowerCase(credString);
|
String nonce = null;
|
boolean useUTF8 = false;
|
int pos = 0;
|
int length = credString.length();
|
while (pos < length)
|
{
|
int equalPos = credString.indexOf('=', pos+1);
|
if (equalPos < 0)
|
{
|
// This is bad because we're not at the end of the string but we don't
|
// have a name/value delimiter.
|
LocalizableMessage message =
|
ERR_LDAPAUTH_DIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get(
|
credString, pos);
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
|
String tokenName = lowerCreds.substring(pos, equalPos);
|
|
StringBuilder valueBuffer = new StringBuilder();
|
pos = readToken(credString, equalPos+1, length, valueBuffer);
|
String tokenValue = valueBuffer.toString();
|
|
if (tokenName.equals("charset"))
|
{
|
// The value must be the string "utf-8". If not, that's an error.
|
if (! tokenValue.equalsIgnoreCase("utf-8"))
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_DIGESTMD5_INVALID_CHARSET.get(tokenValue);
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
useUTF8 = true;
|
}
|
else if (tokenName.equals("realm"))
|
{
|
// This will only be of interest to us if there is only a single realm
|
// in the server credentials and none was provided as a client-side
|
// property.
|
if (! realmSetFromProperty)
|
{
|
if (realm == null)
|
{
|
// No other realm was specified, so we'll use this one for now.
|
realm = tokenValue;
|
}
|
else
|
{
|
// This must mean that there are multiple realms in the server
|
// credentials. In that case, we'll not provide any realm at all.
|
// To make sure that happens, pretend that the client specified the
|
// realm.
|
realm = null;
|
realmSetFromProperty = true;
|
}
|
}
|
}
|
else if (tokenName.equals("nonce"))
|
{
|
nonce = tokenValue;
|
}
|
else if (tokenName.equals("qop"))
|
{
|
// The QoP modes provided by the server should be a comma-delimited
|
// list. Decode that list and make sure the QoP we have chosen is in
|
// that list.
|
StringTokenizer tokenizer = new StringTokenizer(tokenValue, ",");
|
LinkedList<String> qopModes = new LinkedList<String>();
|
while (tokenizer.hasMoreTokens())
|
{
|
qopModes.add(toLowerCase(tokenizer.nextToken().trim()));
|
}
|
|
if (! qopModes.contains(qop))
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_REQUESTED_QOP_NOT_SUPPORTED_BY_SERVER.
|
get(qop, tokenValue);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
}
|
else
|
{
|
// Other values may have been provided, but they aren't of interest to
|
// us because they shouldn't change anything about the way we encode the
|
// second part of the request. Rather than attempt to examine them,
|
// we'll assume that the server sent a valid response.
|
}
|
}
|
|
|
// Make sure that the nonce was included in the response from the server.
|
if (nonce == null)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_NONCE.get();
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
|
// Generate the cnonce that we will use for this request.
|
String cnonce = generateCNonce();
|
|
|
// Generate the response digest, and initialize the necessary remaining
|
// variables to use in the generation of that digest.
|
String nonceCount = "00000001";
|
String charset = (useUTF8 ? "UTF-8" : "ISO-8859-1");
|
String responseDigest;
|
try
|
{
|
responseDigest = generateDigestMD5Response(authID, authzID,
|
bindPassword, realm,
|
nonce, cnonce, nonceCount,
|
digestURI, qop, charset);
|
}
|
catch (ClientException ce)
|
{
|
throw ce;
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_CANNOT_CREATE_RESPONSE_DIGEST.
|
get(getExceptionMessage(e));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
|
}
|
|
|
// Generate the SASL credentials for the second bind request.
|
StringBuilder credBuffer = new StringBuilder();
|
credBuffer.append("username=\"");
|
credBuffer.append(authID);
|
credBuffer.append("\"");
|
|
if (realm != null)
|
{
|
credBuffer.append(",realm=\"");
|
credBuffer.append(realm);
|
credBuffer.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);
|
|
if (useUTF8)
|
{
|
credBuffer.append(",charset=utf-8");
|
}
|
|
if (authzID != null)
|
{
|
credBuffer.append(",authzid=\"");
|
credBuffer.append(authzID);
|
credBuffer.append("\"");
|
}
|
|
|
// Generate and send the second bind request.
|
BindRequestProtocolOp bindRequest2 =
|
new BindRequestProtocolOp(bindDN.toByteString(),
|
SASL_MECHANISM_DIGEST_MD5,
|
ByteString.valueOf(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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
|
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
|
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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);
|
}
|
|
|
// Make sure that the bind response included server SASL credentials with
|
// the appropriate rspauth value.
|
ByteString rspAuthCreds = bindResponse2.getServerSASLCredentials();
|
if (rspAuthCreds == null)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
String credStr = toLowerCase(rspAuthCreds.toString());
|
if (! credStr.startsWith("rspauth="))
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
|
byte[] serverRspAuth;
|
try
|
{
|
serverRspAuth = hexStringToByteArray(credStr.substring(8));
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_DECODE_RSPAUTH.get(
|
getExceptionMessage(e));
|
throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
|
}
|
|
byte[] clientRspAuth;
|
try
|
{
|
clientRspAuth =
|
generateDigestMD5RspAuth(authID, authzID, bindPassword,
|
realm, nonce, cnonce, nonceCount, digestURI,
|
qop, charset);
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_CALCULATE_RSPAUTH.get(
|
getExceptionMessage(e));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
|
}
|
|
if (! Arrays.equals(serverRspAuth, clientRspAuth))
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_RSPAUTH_MISMATCH.get();
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
|
}
|
|
// FIXME -- Need to look for things like password expiration warning,
|
// reset notice, etc.
|
return null;
|
}
|
|
|
|
/**
|
* Reads the next token from the provided credentials string using the
|
* provided information. If the token is surrounded by quotation marks, then
|
* the token returned will not include those quotation marks.
|
*
|
* @param credentials The credentials string from which to read the token.
|
* @param startPos The position of the first character of the token to
|
* read.
|
* @param length The total number of characters in the credentials
|
* string.
|
* @param token The buffer into which the token is to be placed.
|
*
|
* @return The position at which the next token should start, or a value
|
* greater than or equal to the length of the string if there are no
|
* more tokens.
|
*
|
* @throws LDAPException If a problem occurs while attempting to read the
|
* token.
|
*/
|
private int readToken(String credentials, int startPos, int length,
|
StringBuilder token)
|
throws LDAPException
|
{
|
// If the position is greater than or equal to the length, then we shouldn't
|
// do anything.
|
if (startPos >= length)
|
{
|
return startPos;
|
}
|
|
|
// Look at the first character to see if it's an empty string or the string
|
// is quoted.
|
boolean isEscaped = false;
|
boolean isQuoted = false;
|
int pos = startPos;
|
char c = credentials.charAt(pos++);
|
|
if (c == ',')
|
{
|
// This must be a zero-length token, so we'll just return the next
|
// position.
|
return pos;
|
}
|
else if (c == '"')
|
{
|
// The string is quoted, so we'll ignore this character, and we'll keep
|
// reading until we find the unescaped closing quote followed by a comma
|
// or the end of the string.
|
isQuoted = true;
|
}
|
else if (c == '\\')
|
{
|
// The next character is escaped, so we'll take it no matter what.
|
isEscaped = true;
|
}
|
else
|
{
|
// The string is not quoted, and this is the first character. Store this
|
// character and keep reading until we find a comma or the end of the
|
// string.
|
token.append(c);
|
}
|
|
|
// Enter a loop, reading until we find the appropriate criteria for the end
|
// of the token.
|
while (pos < length)
|
{
|
c = credentials.charAt(pos++);
|
|
if (isEscaped)
|
{
|
// The previous character was an escape, so we'll take this no matter
|
// what.
|
token.append(c);
|
isEscaped = false;
|
}
|
else if (c == ',')
|
{
|
// 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
|
{
|
break;
|
}
|
}
|
else if (c == '"')
|
{
|
if (isQuoted)
|
{
|
// This should be the end of the token, but in order for it to be
|
// valid it must be followed by a comma or the end of the string.
|
if (pos >= length)
|
{
|
// We have hit the end of the string, 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);
|
}
|
}
|
}
|
else
|
{
|
// This must be part of the value, so we'll take it.
|
token.append(c);
|
}
|
}
|
else if (c == '\\')
|
{
|
// The next character is escaped. We'll set a flag so we know to
|
// accept it, but will not include the backspace itself.
|
isEscaped = true;
|
}
|
else
|
{
|
token.append(c);
|
}
|
}
|
|
|
return pos;
|
}
|
|
|
|
/**
|
* Generates a cnonce value to use during the DIGEST-MD5 authentication
|
* process.
|
*
|
* @return The cnonce that should be used for DIGEST-MD5 authentication.
|
*/
|
private String generateCNonce()
|
{
|
if (secureRandom == null)
|
{
|
secureRandom = new SecureRandom();
|
}
|
|
byte[] cnonceBytes = new byte[16];
|
secureRandom.nextBytes(cnonceBytes);
|
|
return Base64.encode(cnonceBytes);
|
}
|
|
|
|
/**
|
* Generates the appropriate DIGEST-MD5 response for the provided set of
|
* information.
|
*
|
* @param authID The username from the authentication request.
|
* @param authzID The authorization ID from the request, or
|
* <CODE>null</CODE> if there is none.
|
* @param password The clear-text password for the user.
|
* @param realm The realm for which the authentication is to be
|
* performed.
|
* @param nonce The random data generated by the server for use in the
|
* digest.
|
* @param cnonce The random data generated by the client for use in the
|
* digest.
|
* @param nonceCount The 8-digit hex string indicating the number of times
|
* the provided nonce has been used by the client.
|
* @param digestURI The digest URI that specifies the service and host for
|
* which the authentication is being performed.
|
* @param qop The quality of protection string for the
|
* authentication.
|
* @param charset The character set used to encode the information.
|
*
|
* @return The DIGEST-MD5 response for the provided set of information.
|
*
|
* @throws ClientException If a problem occurs while attempting to
|
* initialize the MD5 digest.
|
*
|
* @throws UnsupportedEncodingException If the specified character set is
|
* invalid for some reason.
|
*/
|
private String generateDigestMD5Response(String authID, String authzID,
|
ByteSequence password, String realm,
|
String nonce, String cnonce,
|
String nonceCount, String digestURI,
|
String qop, String charset)
|
throws ClientException, UnsupportedEncodingException
|
{
|
// Perform the necessary initialization if it hasn't been done yet.
|
if (md5Digest == null)
|
{
|
try
|
{
|
md5Digest = MessageDigest.getInstance("MD5");
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
|
getExceptionMessage(e));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
|
message, e);
|
}
|
}
|
|
|
// 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);
|
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(':');
|
a1String2.append(nonce);
|
a1String2.append(':');
|
a1String2.append(cnonce);
|
if (authzID != null)
|
{
|
a1String2.append(':');
|
a1String2.append(authzID);
|
}
|
byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
|
byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length];
|
System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
|
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)));
|
}
|
|
|
|
/**
|
* Generates the appropriate DIGEST-MD5 rspauth digest using the provided
|
* information.
|
*
|
* @param authID The username from the authentication request.
|
* @param authzID The authorization ID from the request, or
|
* <CODE>null</CODE> if there is none.
|
* @param password The clear-text password for the user.
|
* @param realm The realm for which the authentication is to be
|
* performed.
|
* @param nonce The random data generated by the server for use in the
|
* digest.
|
* @param cnonce The random data generated by the client for use in the
|
* digest.
|
* @param nonceCount The 8-digit hex string indicating the number of times
|
* the provided nonce has been used by the client.
|
* @param digestURI The digest URI that specifies the service and host for
|
* which the authentication is being performed.
|
* @param qop The quality of protection string for the
|
* authentication.
|
* @param charset The character set used to encode the information.
|
*
|
* @return The DIGEST-MD5 response for the provided set of information.
|
*
|
* @throws UnsupportedEncodingException If the specified character set is
|
* invalid for some reason.
|
*/
|
public byte[] generateDigestMD5RspAuth(String authID, String authzID,
|
ByteSequence password, String realm,
|
String nonce, String cnonce,
|
String nonceCount, String digestURI,
|
String qop, String charset)
|
throws UnsupportedEncodingException
|
{
|
// First, get a hash of "username:realm:password".
|
StringBuilder a1String1 = new StringBuilder();
|
a1String1.append(authID);
|
a1String1.append(':');
|
a1String1.append(realm);
|
a1String1.append(':');
|
|
byte[] a1Bytes1a = a1String1.toString().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(':');
|
a1String2.append(nonce);
|
a1String2.append(':');
|
a1String2.append(cnonce);
|
if (authzID != null)
|
{
|
a1String2.append(':');
|
a1String2.append(authzID);
|
}
|
byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
|
byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length];
|
System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
|
System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length,
|
a1Bytes2a.length);
|
byte[] a1Hash = md5Digest.digest(a1Bytes2);
|
|
|
// Next, get a hash of "AUTHENTICATE:digesturi".
|
String a2String = ":" + digestURI;
|
if (qop.equals("auth-int") || qop.equals("auth-conf"))
|
{
|
a2String += ":00000000000000000000000000000000";
|
}
|
byte[] a2Bytes = a2String.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 md5Digest.digest(kdStr.toString().getBytes(charset));
|
}
|
|
|
|
/**
|
* Retrieves a hexadecimal string representation of the contents of the
|
* provided byte array.
|
*
|
* @param byteArray The byte array for which to obtain the hexadecimal
|
* string representation.
|
*
|
* @return The hexadecimal string representation of the contents of the
|
* provided byte array.
|
*/
|
private String getHexString(byte[] byteArray)
|
{
|
StringBuilder buffer = new StringBuilder(2*byteArray.length);
|
for (byte b : byteArray)
|
{
|
buffer.append(byteToLowerHex(b));
|
}
|
|
return buffer.toString();
|
}
|
|
|
|
/**
|
* Retrieves the set of properties that a client may provide when performing a
|
* SASL DIGEST-MD5 bind, mapped from the property names to their corresponding
|
* descriptions.
|
*
|
* @return The set of properties that a client may provide when performing a
|
* SASL DIGEST-MD5 bind, mapped from the property names to their
|
* corresponding descriptions.
|
*/
|
public static LinkedHashMap<String,LocalizableMessage> getSASLDigestMD5Properties()
|
{
|
LinkedHashMap<String,LocalizableMessage> properties =
|
new LinkedHashMap<String,LocalizableMessage>(5);
|
|
properties.put(SASL_PROPERTY_AUTHID,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
|
properties.put(SASL_PROPERTY_REALM,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
|
properties.put(SASL_PROPERTY_QOP,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_QOP.get());
|
properties.put(SASL_PROPERTY_DIGEST_URI,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_DIGEST_URI.get());
|
properties.put(SASL_PROPERTY_AUTHZID,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
|
|
return properties;
|
}
|
|
|
|
/**
|
* Processes a SASL EXTERNAL bind with the provided information.
|
*
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if the authentication identity
|
* is to be set through some other means.
|
* @param saslProperties A set of additional properties that may be needed
|
* to process the SASL bind. SASL EXTERNAL does not
|
* take any properties, so this should be empty or
|
* <CODE>null</CODE>.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSASLExternal(ByteSequence bindDN,
|
Map<String,List<String>> saslProperties,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
// Make sure that no SASL properties were provided.
|
if ((saslProperties != null) && (! saslProperties.isEmpty()))
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_NO_ALLOWED_SASL_PROPERTIES.get(SASL_MECHANISM_EXTERNAL);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
|
// 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);
|
|
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 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 (LDAPException le)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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);
|
}
|
|
|
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_EXTERNAL);
|
throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
|
message, bindResponse.getMatchedDN(), null);
|
}
|
|
|
|
/**
|
* Retrieves the set of properties that a client may provide when performing a
|
* SASL EXTERNAL bind, mapped from the property names to their corresponding
|
* descriptions.
|
*
|
* @return The set of properties that a client may provide when performing a
|
* SASL EXTERNAL bind, mapped from the property names to their
|
* corresponding descriptions.
|
*/
|
public static LinkedHashMap<String,LocalizableMessage> getSASLExternalProperties()
|
{
|
// There are no properties for the SASL EXTERNAL mechanism.
|
return new LinkedHashMap<String,LocalizableMessage>(0);
|
}
|
|
|
|
/**
|
* Processes a SASL GSSAPI bind with the provided information.
|
*
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if the authentication identity
|
* is to be set through some other means.
|
* @param bindPassword The password to use to bind to the Directory
|
* Server.
|
* @param saslProperties A set of additional properties that may be needed
|
* to process the SASL bind. SASL EXTERNAL does not
|
* take any properties, so this should be empty or
|
* <CODE>null</CODE>.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSASLGSSAPI(ByteSequence bindDN,
|
ByteSequence bindPassword,
|
Map<String,List<String>> saslProperties,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
String kdc = null;
|
String realm = null;
|
|
gssapiBindDN = bindDN;
|
gssapiAuthID = null;
|
gssapiAuthzID = null;
|
gssapiQoP = "auth";
|
|
if (bindPassword == null)
|
{
|
gssapiAuthPW = null;
|
}
|
else
|
{
|
gssapiAuthPW = bindPassword.toString().toCharArray();
|
}
|
|
|
// Evaluate the properties provided. The authID is required. The authzID,
|
// KDC, QoP, and realm are optional.
|
if ((saslProperties == null) || saslProperties.isEmpty())
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_GSSAPI);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
Iterator<String> propertyNames = saslProperties.keySet().iterator();
|
while (propertyNames.hasNext())
|
{
|
String name = propertyNames.next();
|
String lowerName = toLowerCase(name);
|
|
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);
|
}
|
}
|
}
|
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);
|
}
|
}
|
}
|
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);
|
}
|
}
|
}
|
else if (lowerName.equals(SASL_PROPERTY_QOP))
|
{
|
List<String> values = saslProperties.get(name);
|
Iterator<String> iterator = values.iterator();
|
if (iterator.hasNext())
|
{
|
gssapiQoP = toLowerCase(iterator.next());
|
|
if (iterator.hasNext())
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
|
if (gssapiQoP.equals("auth"))
|
{
|
// This is always fine.
|
}
|
else if (gssapiQoP.equals("auth-int") ||
|
gssapiQoP.equals("auth-conf"))
|
{
|
// FIXME -- Add support for integrity and confidentiality.
|
LocalizableMessage message =
|
ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(gssapiQoP);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
else
|
{
|
// This is an illegal value.
|
LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_INVALID_QOP.get(gssapiQoP);
|
throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
|
message);
|
}
|
}
|
}
|
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);
|
}
|
}
|
}
|
else
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_GSSAPI);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
}
|
|
|
// Make sure that the authID was provided.
|
if ((gssapiAuthID == null) || (gssapiAuthID.length() == 0))
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_GSSAPI);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
|
// See if an authzID was provided. If not, then use the authID.
|
if (gssapiAuthzID == null)
|
{
|
gssapiAuthzID = gssapiAuthID;
|
}
|
|
|
// See if the realm and/or KDC were specified. If so, then set properties
|
// that will allow them to be used. Otherwise, we'll hope that the
|
// underlying system has a valid Kerberos client configuration.
|
if (realm != null)
|
{
|
System.setProperty(KRBV_PROPERTY_REALM, realm);
|
}
|
|
if (kdc != null)
|
{
|
System.setProperty(KRBV_PROPERTY_KDC, kdc);
|
}
|
|
|
// Since we're going to be using JAAS behind the scenes, we need to have a
|
// JAAS configuration. Rather than always requiring the user to provide it,
|
// we'll write one to a temporary file that will be deleted when the JVM
|
// exits.
|
String configFileName;
|
try
|
{
|
File tempFile = File.createTempFile("login", "conf");
|
configFileName = tempFile.getAbsolutePath();
|
tempFile.deleteOnExit();
|
BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false));
|
|
w.write(getClass().getName() + " {");
|
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();
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get(
|
getExceptionMessage(e));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
|
}
|
|
System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName);
|
System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "true");
|
|
|
// The rest of this code must be executed via JAAS, so it will have to go
|
// in the "run" method.
|
LoginContext loginContext;
|
try
|
{
|
loginContext = new LoginContext(getClass().getName(), this);
|
loginContext.login();
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED.get(
|
getExceptionMessage(e));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
|
}
|
|
try
|
{
|
Subject.doAs(loginContext.getSubject(), this);
|
}
|
catch (Exception e)
|
{
|
if (e instanceof ClientException)
|
{
|
throw (ClientException) e;
|
}
|
else if (e instanceof LDAPException)
|
{
|
throw (LDAPException) e;
|
}
|
|
LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_REMOTE_AUTHENTICATION_FAILED.get(
|
getExceptionMessage(e));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
|
}
|
|
|
// 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;
|
}
|
|
|
|
/**
|
* Retrieves the set of properties that a client may provide when performing a
|
* SASL EXTERNAL bind, mapped from the property names to their corresponding
|
* descriptions.
|
*
|
* @return The set of properties that a client may provide when performing a
|
* SASL EXTERNAL bind, mapped from the property names to their
|
* corresponding descriptions.
|
*/
|
public static LinkedHashMap<String,LocalizableMessage> getSASLGSSAPIProperties()
|
{
|
LinkedHashMap<String,LocalizableMessage> properties =
|
new LinkedHashMap<String,LocalizableMessage>(4);
|
|
properties.put(SASL_PROPERTY_AUTHID,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
|
properties.put(SASL_PROPERTY_AUTHZID,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
|
properties.put(SASL_PROPERTY_KDC,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_KDC.get());
|
properties.put(SASL_PROPERTY_REALM,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
|
|
return properties;
|
}
|
|
|
|
/**
|
* Processes a SASL PLAIN bind with the provided information.
|
*
|
* @param bindDN The DN to use to bind to the Directory Server, or
|
* <CODE>null</CODE> if the authentication identity
|
* is to be set through some other means.
|
* @param bindPassword The password to use to bind to the Directory
|
* Server.
|
* @param saslProperties A set of additional properties that may be needed
|
* to process the SASL bind.
|
* @param requestControls The set of controls to include the request to the
|
* server.
|
* @param responseControls A list to hold the set of controls included in
|
* the response from the server.
|
*
|
* @return A message providing additional information about the bind if
|
* appropriate, or <CODE>null</CODE> if there is no special
|
* information available.
|
*
|
* @throws ClientException If a client-side problem prevents the bind
|
* attempt from succeeding.
|
*
|
* @throws LDAPException If the bind fails or some other server-side problem
|
* occurs during processing.
|
*/
|
public String doSASLPlain(ByteSequence bindDN,
|
ByteSequence bindPassword,
|
Map<String,List<String>> saslProperties,
|
List<Control> requestControls,
|
List<Control> responseControls)
|
throws ClientException, LDAPException
|
{
|
String authID = null;
|
String authzID = null;
|
|
|
// Evaluate the properties provided. The authID is required, and authzID is
|
// optional.
|
if ((saslProperties == null) || saslProperties.isEmpty())
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_PLAIN);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
Iterator<String> propertyNames = saslProperties.keySet().iterator();
|
while (propertyNames.hasNext())
|
{
|
String name = propertyNames.next();
|
String lowerName = toLowerCase(name);
|
|
if (lowerName.equals(SASL_PROPERTY_AUTHID))
|
{
|
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);
|
}
|
}
|
}
|
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);
|
}
|
}
|
}
|
else
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_PLAIN);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
}
|
|
|
// Make sure that at least the authID was provided.
|
if ((authID == null) || (authID.length() == 0))
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_PLAIN);
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
|
}
|
|
|
// Set password to ByteString.empty if the password is null.
|
if (bindPassword == null)
|
{
|
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());
|
|
ByteString saslCredentials =
|
ByteString.valueOf(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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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);
|
}
|
|
|
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);
|
}
|
|
|
|
/**
|
* Retrieves the set of properties that a client may provide when performing a
|
* SASL PLAIN bind, mapped from the property names to their corresponding
|
* descriptions.
|
*
|
* @return The set of properties that a client may provide when performing a
|
* SASL PLAIN bind, mapped from the property names to their
|
* corresponding descriptions.
|
*/
|
public static LinkedHashMap<String,LocalizableMessage> getSASLPlainProperties()
|
{
|
LinkedHashMap<String,LocalizableMessage> properties =
|
new LinkedHashMap<String,LocalizableMessage>(2);
|
|
properties.put(SASL_PROPERTY_AUTHID,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
|
properties.put(SASL_PROPERTY_AUTHZID,
|
INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
|
|
return properties;
|
}
|
|
|
|
/**
|
* Performs a privileged operation under JAAS so that the local authentication
|
* information can be available for the SASL bind to the Directory Server.
|
*
|
* @return A placeholder object in order to comply with the
|
* <CODE>PrivilegedExceptionAction</CODE> interface.
|
*
|
* @throws ClientException If a client-side problem occurs during the bind
|
* processing.
|
*
|
* @throws LDAPException If a server-side problem occurs during the bind
|
* processing.
|
*/
|
@Override
|
public Object run()
|
throws ClientException, LDAPException
|
{
|
if (saslMechanism == null)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_NONSASL_RUN_INVOCATION.get(getBacktrace());
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
|
}
|
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<String,String>();
|
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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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.
|
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);
|
}
|
|
|
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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE
|
.get(getExceptionMessage(ae));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
|
getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
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.
|
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);
|
}
|
}
|
else
|
{
|
// This is an error.
|
LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get();
|
throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
|
message, bindResponse.getMatchedDN(),
|
null);
|
}
|
}
|
}
|
else
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RUN_INVOCATION.get(
|
saslMechanism, getBacktrace());
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
|
}
|
|
|
// FIXME -- Need to look for things like password expiration warning, reset
|
// notice, etc.
|
return null;
|
}
|
|
|
|
/**
|
* Handles the authentication callbacks to provide information needed by the
|
* JAAS login process.
|
*
|
* @param callbacks The callbacks needed to provide information for the JAAS
|
* login process.
|
*
|
* @throws UnsupportedCallbackException If an unexpected callback is
|
* included in the provided set.
|
*/
|
@Override
|
public void handle(Callback[] callbacks)
|
throws UnsupportedCallbackException
|
{
|
if (saslMechanism == null)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_NONSASL_CALLBACK_INVOCATION.get(getBacktrace());
|
throw new UnsupportedCallbackException(callbacks[0], message.toString());
|
}
|
else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
|
{
|
for (Callback cb : callbacks)
|
{
|
if (cb instanceof NameCallback)
|
{
|
((NameCallback) cb).setName(gssapiAuthID);
|
}
|
else if (cb instanceof PasswordCallback)
|
{
|
if (gssapiAuthPW == null)
|
{
|
System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(gssapiAuthID));
|
gssapiAuthPW = PasswordReader.readPassword();
|
}
|
|
((PasswordCallback) cb).setPassword(gssapiAuthPW);
|
}
|
else
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_GSSAPI_CALLBACK.get(cb);
|
throw new UnsupportedCallbackException(cb, message.toString());
|
}
|
}
|
}
|
else
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_CALLBACK_INVOCATION.get(
|
saslMechanism, getBacktrace());
|
throw new UnsupportedCallbackException(callbacks[0], message.toString());
|
}
|
}
|
|
|
|
/**
|
* Uses the "Who Am I?" extended operation to request that the server provide
|
* the client with the authorization identity for this connection.
|
*
|
* @return An ASN.1 octet string containing the authorization identity, or
|
* <CODE>null</CODE> if the client is not authenticated or is
|
* authenticated anonymously.
|
*
|
* @throws ClientException If a client-side problem occurs during the
|
* request processing.
|
*
|
* @throws LDAPException If a server-side problem occurs during the request
|
* processing.
|
*/
|
public ByteString requestAuthorizationIdentity()
|
throws ClientException, LDAPException
|
{
|
// Construct the extended request and send it to the server.
|
ExtendedRequestProtocolOp extendedRequest =
|
new ExtendedRequestProtocolOp(OID_WHO_AM_I_REQUEST);
|
LDAPMessage requestMessage =
|
new LDAPMessage(nextMessageID.getAndIncrement(), extendedRequest);
|
|
try
|
{
|
writer.writeMessage(requestMessage);
|
}
|
catch (IOException ioe)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(ioe));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
|
message, ioe);
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.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 ae)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(ae));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, ae);
|
}
|
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 (LDAPException le)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(le));
|
throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR,
|
message, le);
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage message =
|
ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e));
|
throw new ClientException(
|
ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
|
}
|
|
|
// If the protocol op isn't an extended response, then that's a problem.
|
if (responseMessage.getProtocolOpType() != OP_TYPE_EXTENDED_RESPONSE)
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp());
|
throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
|
}
|
|
|
// Get the extended response and see if it has the "notice of disconnection"
|
// OID. If so, then the server is closing the connection.
|
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);
|
}
|
|
|
// It isn't a notice of disconnection so it must be the "Who Am I?"
|
// response and the value would be the authorization ID. However, first
|
// check that it was successful. If it was not, then fail.
|
int resultCode = extendedResponse.getResultCode();
|
if (resultCode != ReturnCode.SUCCESS.get())
|
{
|
LocalizableMessage message = ERR_LDAPAUTH_WHOAMI_FAILED.get();
|
throw new LDAPException(resultCode, extendedResponse.getErrorMessage(),
|
message, extendedResponse.getMatchedDN(),
|
null);
|
}
|
|
|
// Get the authorization ID (if there is one) and return it to the caller.
|
ByteString authzID = extendedResponse.getValue();
|
if ((authzID == null) || (authzID.length() == 0))
|
{
|
return null;
|
}
|
|
String valueString = authzID.toString();
|
if ((valueString == null) || (valueString.length() == 0) ||
|
valueString.equalsIgnoreCase("dn:"))
|
{
|
return null;
|
}
|
|
return authzID;
|
}
|
}
|