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