001 /*
002 * Copyright 2007-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2014 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.ldap.sdk;
022
023
024
025 import java.util.ArrayList;
026 import java.util.Arrays;
027 import java.util.Collections;
028 import java.util.HashMap;
029 import java.util.List;
030 import java.util.logging.Level;
031 import javax.security.auth.callback.Callback;
032 import javax.security.auth.callback.CallbackHandler;
033 import javax.security.auth.callback.NameCallback;
034 import javax.security.auth.callback.PasswordCallback;
035 import javax.security.sasl.RealmCallback;
036 import javax.security.sasl.RealmChoiceCallback;
037 import javax.security.sasl.Sasl;
038 import javax.security.sasl.SaslClient;
039
040 import com.unboundid.asn1.ASN1OctetString;
041 import com.unboundid.util.DebugType;
042 import com.unboundid.util.InternalUseOnly;
043 import com.unboundid.util.NotMutable;
044 import com.unboundid.util.ThreadSafety;
045 import com.unboundid.util.ThreadSafetyLevel;
046
047 import static com.unboundid.ldap.sdk.LDAPMessages.*;
048 import static com.unboundid.util.Debug.*;
049 import static com.unboundid.util.StaticUtils.*;
050 import static com.unboundid.util.Validator.*;
051
052
053
054 /**
055 * This class provides a SASL DIGEST-MD5 bind request implementation as
056 * described in <A HREF="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831</A>. The
057 * DIGEST-MD5 mechanism can be used to authenticate over an insecure channel
058 * without exposing the credentials (although it requires that the server have
059 * access to the clear-text password). It is similar to CRAM-MD5, but provides
060 * better security by combining random data from both the client and the server,
061 * and allows for greater security and functionality, including the ability to
062 * specify an alternate authorization identity and the ability to use data
063 * integrity or confidentiality protection. At present, however, this
064 * implementation may only be used for authentication, as it does not yet
065 * support integrity or confidentiality.
066 * <BR><BR>
067 * Elements included in a DIGEST-MD5 bind request include:
068 * <UL>
069 * <LI>Authentication ID -- A string which identifies the user that is
070 * attempting to authenticate. It should be an "authzId" value as
071 * described in section 5.2.1.8 of
072 * <A HREF="http://www.ietf.org/rfc/rfc4513.txt">RFC 4513</A>. That is,
073 * it should be either "dn:" followed by the distinguished name of the
074 * target user, or "u:" followed by the username. If the "u:" form is
075 * used, then the mechanism used to resolve the provided username to an
076 * entry may vary from server to server.</LI>
077 * <LI>Authorization ID -- An optional string which specifies an alternate
078 * authorization identity that should be used for subsequent operations
079 * requested on the connection. Like the authentication ID, the
080 * authorization ID should use the "authzId" syntax.</LI>
081 * <LI>Realm -- An optional string which specifies the realm into which the
082 * user should authenticate.</LI>
083 * <LI>Password -- The clear-text password for the target user.</LI>
084 * </UL>
085 * <H2>Example</H2>
086 * The following example demonstrates the process for performing a DIGEST-MD5
087 * bind against a directory server with a username of "john.doe" and a password
088 * of "password":
089 * <PRE>
090 * DIGESTMD5BindRequest bindRequest =
091 * new DIGESTMD5BindRequest("u:john.doe", "password");
092 * BindResult bindResult;
093 * try
094 * {
095 * bindResult = connection.bind(bindRequest);
096 * // If we get here, then the bind was successful.
097 * }
098 * catch (LDAPException le)
099 * {
100 * // The bind failed for some reason.
101 * bindResult = new BindResult(le.toLDAPResult());
102 * ResultCode resultCode = le.getResultCode();
103 * String errorMessageFromServer = le.getDiagnosticMessage();
104 * }
105 * </PRE>
106 */
107 @NotMutable()
108 @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
109 public final class DIGESTMD5BindRequest
110 extends SASLBindRequest
111 implements CallbackHandler
112 {
113 /**
114 * The name for the DIGEST-MD5 SASL mechanism.
115 */
116 public static final String DIGESTMD5_MECHANISM_NAME = "DIGEST-MD5";
117
118
119
120 /**
121 * The serial version UID for this serializable class.
122 */
123 private static final long serialVersionUID = 867592367640540593L;
124
125
126
127 // The password for this bind request.
128 private final ASN1OctetString password;
129
130 // The message ID from the last LDAP message sent from this request.
131 private int messageID = -1;
132
133 // The SASL quality of protection value(s) allowed for the DIGEST-MD5 bind
134 // request.
135 private final List<SASLQualityOfProtection> allowedQoP;
136
137 // A list that will be updated with messages about any unhandled callbacks
138 // encountered during processing.
139 private final List<String> unhandledCallbackMessages;
140
141 // The authentication ID string for this bind request.
142 private final String authenticationID;
143
144 // The authorization ID string for this bind request, if available.
145 private final String authorizationID;
146
147 // The realm form this bind request, if available.
148 private final String realm;
149
150
151
152 /**
153 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
154 * ID and password. It will not include an authorization ID, a realm, or any
155 * controls.
156 *
157 * @param authenticationID The authentication ID for this bind request. It
158 * must not be {@code null}.
159 * @param password The password for this bind request. It must not
160 * be {@code null}.
161 */
162 public DIGESTMD5BindRequest(final String authenticationID,
163 final String password)
164 {
165 this(authenticationID, null, new ASN1OctetString(password), null,
166 NO_CONTROLS);
167
168 ensureNotNull(password);
169 }
170
171
172
173 /**
174 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
175 * ID and password. It will not include an authorization ID, a realm, or any
176 * controls.
177 *
178 * @param authenticationID The authentication ID for this bind request. It
179 * must not be {@code null}.
180 * @param password The password for this bind request. It must not
181 * be {@code null}.
182 */
183 public DIGESTMD5BindRequest(final String authenticationID,
184 final byte[] password)
185 {
186 this(authenticationID, null, new ASN1OctetString(password), null,
187 NO_CONTROLS);
188
189 ensureNotNull(password);
190 }
191
192
193
194 /**
195 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
196 * ID and password. It will not include an authorization ID, a realm, or any
197 * controls.
198 *
199 * @param authenticationID The authentication ID for this bind request. It
200 * must not be {@code null}.
201 * @param password The password for this bind request. It must not
202 * be {@code null}.
203 */
204 public DIGESTMD5BindRequest(final String authenticationID,
205 final ASN1OctetString password)
206 {
207 this(authenticationID, null, password, null, NO_CONTROLS);
208 }
209
210
211
212 /**
213 * Creates a new SASL DIGEST-MD5 bind request with the provided information.
214 *
215 * @param authenticationID The authentication ID for this bind request. It
216 * must not be {@code null}.
217 * @param authorizationID The authorization ID for this bind request. It
218 * may be {@code null} if there will not be an
219 * alternate authorization identity.
220 * @param password The password for this bind request. It must not
221 * be {@code null}.
222 * @param realm The realm to use for the authentication. It may
223 * be {@code null} if the server supports a default
224 * realm.
225 * @param controls The set of controls to include in the request.
226 */
227 public DIGESTMD5BindRequest(final String authenticationID,
228 final String authorizationID,
229 final String password, final String realm,
230 final Control... controls)
231 {
232 this(authenticationID, authorizationID, new ASN1OctetString(password),
233 realm, controls);
234
235 ensureNotNull(password);
236 }
237
238
239
240 /**
241 * Creates a new SASL DIGEST-MD5 bind request with the provided information.
242 *
243 * @param authenticationID The authentication ID for this bind request. It
244 * must not be {@code null}.
245 * @param authorizationID The authorization ID for this bind request. It
246 * may be {@code null} if there will not be an
247 * alternate authorization identity.
248 * @param password The password for this bind request. It must not
249 * be {@code null}.
250 * @param realm The realm to use for the authentication. It may
251 * be {@code null} if the server supports a default
252 * realm.
253 * @param controls The set of controls to include in the request.
254 */
255 public DIGESTMD5BindRequest(final String authenticationID,
256 final String authorizationID,
257 final byte[] password, final String realm,
258 final Control... controls)
259 {
260 this(authenticationID, authorizationID, new ASN1OctetString(password),
261 realm, controls);
262
263 ensureNotNull(password);
264 }
265
266
267
268 /**
269 * Creates a new SASL DIGEST-MD5 bind request with the provided information.
270 *
271 * @param authenticationID The authentication ID for this bind request. It
272 * must not be {@code null}.
273 * @param authorizationID The authorization ID for this bind request. It
274 * may be {@code null} if there will not be an
275 * alternate authorization identity.
276 * @param password The password for this bind request. It must not
277 * be {@code null}.
278 * @param realm The realm to use for the authentication. It may
279 * be {@code null} if the server supports a default
280 * realm.
281 * @param controls The set of controls to include in the request.
282 */
283 public DIGESTMD5BindRequest(final String authenticationID,
284 final String authorizationID,
285 final ASN1OctetString password,
286 final String realm, final Control... controls)
287 {
288 super(controls);
289
290 ensureNotNull(authenticationID, password);
291
292 this.authenticationID = authenticationID;
293 this.authorizationID = authorizationID;
294 this.password = password;
295 this.realm = realm;
296
297 allowedQoP = Collections.unmodifiableList(
298 Arrays.asList(SASLQualityOfProtection.AUTH));
299
300 unhandledCallbackMessages = new ArrayList<String>(5);
301 }
302
303
304
305 /**
306 * Creates a new SASL DIGEST-MD5 bind request with the provided set of
307 * properties.
308 *
309 * @param properties The properties to use for this
310 * @param controls The set of controls to include in the request.
311 */
312 public DIGESTMD5BindRequest(final DIGESTMD5BindRequestProperties properties,
313 final Control... controls)
314 {
315 super(controls);
316
317 ensureNotNull(properties);
318
319 authenticationID = properties.getAuthenticationID();
320 authorizationID = properties.getAuthorizationID();
321 password = properties.getPassword();
322 realm = properties.getRealm();
323 allowedQoP = properties.getAllowedQoP();
324
325 unhandledCallbackMessages = new ArrayList<String>(5);
326 }
327
328
329
330 /**
331 * {@inheritDoc}
332 */
333 @Override()
334 public String getSASLMechanismName()
335 {
336 return DIGESTMD5_MECHANISM_NAME;
337 }
338
339
340
341 /**
342 * Retrieves the authentication ID for this bind request.
343 *
344 * @return The authentication ID for this bind request.
345 */
346 public String getAuthenticationID()
347 {
348 return authenticationID;
349 }
350
351
352
353 /**
354 * Retrieves the authorization ID for this bind request, if any.
355 *
356 * @return The authorization ID for this bind request, or {@code null} if
357 * there should not be a separate authorization identity.
358 */
359 public String getAuthorizationID()
360 {
361 return authorizationID;
362 }
363
364
365
366 /**
367 * Retrieves the string representation of the password for this bind request.
368 *
369 * @return The string representation of the password for this bind request.
370 */
371 public String getPasswordString()
372 {
373 return password.stringValue();
374 }
375
376
377
378 /**
379 * Retrieves the bytes that comprise the the password for this bind request.
380 *
381 * @return The bytes that comprise the password for this bind request.
382 */
383 public byte[] getPasswordBytes()
384 {
385 return password.getValue();
386 }
387
388
389
390 /**
391 * Retrieves the realm for this bind request, if any.
392 *
393 * @return The realm for this bind request, or {@code null} if none was
394 * defined and the server should use the default realm.
395 */
396 public String getRealm()
397 {
398 return realm;
399 }
400
401
402
403 /**
404 * Retrieves the list of allowed qualities of protection that may be used for
405 * communication that occurs on the connection after the authentication has
406 * completed, in order from most preferred to least preferred.
407 *
408 * @return The list of allowed qualities of protection that may be used for
409 * communication that occurs on the connection after the
410 * authentication has completed, in order from most preferred to
411 * least preferred.
412 */
413 public List<SASLQualityOfProtection> getAllowedQoP()
414 {
415 return allowedQoP;
416 }
417
418
419
420 /**
421 * Sends this bind request to the target server over the provided connection
422 * and returns the corresponding response.
423 *
424 * @param connection The connection to use to send this bind request to the
425 * server and read the associated response.
426 * @param depth The current referral depth for this request. It should
427 * always be one for the initial request, and should only
428 * be incremented when following referrals.
429 *
430 * @return The bind response read from the server.
431 *
432 * @throws LDAPException If a problem occurs while sending the request or
433 * reading the response.
434 */
435 @Override()
436 protected BindResult process(final LDAPConnection connection, final int depth)
437 throws LDAPException
438 {
439 unhandledCallbackMessages.clear();
440
441 final String[] mechanisms = { DIGESTMD5_MECHANISM_NAME };
442
443 final HashMap<String,Object> saslProperties = new HashMap<String,Object>();
444 saslProperties.put(Sasl.QOP, SASLQualityOfProtection.toString(allowedQoP));
445 saslProperties.put(Sasl.SERVER_AUTH, "false");
446
447 final SaslClient saslClient;
448 try
449 {
450 saslClient = Sasl.createSaslClient(mechanisms, authorizationID, "ldap",
451 connection.getConnectedAddress(),
452 saslProperties, this);
453 }
454 catch (Exception e)
455 {
456 debugException(e);
457 throw new LDAPException(ResultCode.LOCAL_ERROR,
458 ERR_DIGESTMD5_CANNOT_CREATE_SASL_CLIENT.get(getExceptionMessage(e)),
459 e);
460 }
461
462 final SASLHelper helper = new SASLHelper(this, connection,
463 DIGESTMD5_MECHANISM_NAME, saslClient, getControls(),
464 getResponseTimeoutMillis(connection), unhandledCallbackMessages);
465
466 try
467 {
468 return helper.processSASLBind();
469 }
470 finally
471 {
472 messageID = helper.getMessageID();
473 }
474 }
475
476
477
478 /**
479 * {@inheritDoc}
480 */
481 @Override()
482 public DIGESTMD5BindRequest getRebindRequest(final String host,
483 final int port)
484 {
485 final DIGESTMD5BindRequestProperties properties =
486 new DIGESTMD5BindRequestProperties(authenticationID, password);
487 properties.setAuthorizationID(authorizationID);
488 properties.setRealm(realm);
489 properties.setAllowedQoP(allowedQoP);
490
491 return new DIGESTMD5BindRequest(properties, getControls());
492 }
493
494
495
496 /**
497 * Handles any necessary callbacks required for SASL authentication.
498 *
499 * @param callbacks The set of callbacks to be handled.
500 */
501 @InternalUseOnly()
502 public void handle(final Callback[] callbacks)
503 {
504 for (final Callback callback : callbacks)
505 {
506 if (callback instanceof NameCallback)
507 {
508 ((NameCallback) callback).setName(authenticationID);
509 }
510 else if (callback instanceof PasswordCallback)
511 {
512 ((PasswordCallback) callback).setPassword(
513 password.stringValue().toCharArray());
514 }
515 else if (callback instanceof RealmCallback)
516 {
517 final RealmCallback rc = (RealmCallback) callback;
518 if (realm == null)
519 {
520 final String defaultRealm = rc.getDefaultText();
521 if (defaultRealm == null)
522 {
523 unhandledCallbackMessages.add(
524 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get(
525 String.valueOf(rc.getPrompt())));
526 }
527 else
528 {
529 rc.setText(defaultRealm);
530 }
531 }
532 else
533 {
534 rc.setText(realm);
535 }
536 }
537 else if (callback instanceof RealmChoiceCallback)
538 {
539 final RealmChoiceCallback rcc = (RealmChoiceCallback) callback;
540 if (realm == null)
541 {
542 final String choices =
543 concatenateStrings("{", " '", ",", "'", " }", rcc.getChoices());
544 unhandledCallbackMessages.add(
545 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get(
546 rcc.getPrompt(), choices));
547 }
548 else
549 {
550 final String[] choices = rcc.getChoices();
551 for (int i=0; i < choices.length; i++)
552 {
553 if (choices[i].equals(realm))
554 {
555 rcc.setSelectedIndex(i);
556 break;
557 }
558 }
559 }
560 }
561 else
562 {
563 // This is an unexpected callback.
564 if (debugEnabled(DebugType.LDAP))
565 {
566 debug(Level.WARNING, DebugType.LDAP,
567 "Unexpected DIGEST-MD5 SASL callback of type " +
568 callback.getClass().getName());
569 }
570
571 unhandledCallbackMessages.add(ERR_DIGESTMD5_UNEXPECTED_CALLBACK.get(
572 callback.getClass().getName()));
573 }
574 }
575 }
576
577
578
579 /**
580 * {@inheritDoc}
581 */
582 @Override()
583 public int getLastMessageID()
584 {
585 return messageID;
586 }
587
588
589
590 /**
591 * {@inheritDoc}
592 */
593 @Override()
594 public DIGESTMD5BindRequest duplicate()
595 {
596 return duplicate(getControls());
597 }
598
599
600
601 /**
602 * {@inheritDoc}
603 */
604 @Override()
605 public DIGESTMD5BindRequest duplicate(final Control[] controls)
606 {
607 final DIGESTMD5BindRequestProperties properties =
608 new DIGESTMD5BindRequestProperties(authenticationID, password);
609 properties.setAuthorizationID(authorizationID);
610 properties.setRealm(realm);
611 properties.setAllowedQoP(allowedQoP);
612
613 final DIGESTMD5BindRequest bindRequest =
614 new DIGESTMD5BindRequest(properties, controls);
615 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
616 return bindRequest;
617 }
618
619
620
621 /**
622 * {@inheritDoc}
623 */
624 @Override()
625 public void toString(final StringBuilder buffer)
626 {
627 buffer.append("DIGESTMD5BindRequest(authenticationID='");
628 buffer.append(authenticationID);
629 buffer.append('\'');
630
631 if (authorizationID != null)
632 {
633 buffer.append(", authorizationID='");
634 buffer.append(authorizationID);
635 buffer.append('\'');
636 }
637
638 if (realm != null)
639 {
640 buffer.append(", realm='");
641 buffer.append(realm);
642 buffer.append('\'');
643 }
644
645 buffer.append(", qop='");
646 buffer.append(SASLQualityOfProtection.toString(allowedQoP));
647 buffer.append('\'');
648
649 final Control[] controls = getControls();
650 if (controls.length > 0)
651 {
652 buffer.append(", controls={");
653 for (int i=0; i < controls.length; i++)
654 {
655 if (i > 0)
656 {
657 buffer.append(", ");
658 }
659
660 buffer.append(controls[i]);
661 }
662 buffer.append('}');
663 }
664
665 buffer.append(')');
666 }
667 }