001 /*
002 * Copyright 2007-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 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.
064 * <BR><BR>
065 * Elements included in a DIGEST-MD5 bind request include:
066 * <UL>
067 * <LI>Authentication ID -- A string which identifies the user that is
068 * attempting to authenticate. It should be an "authzId" value as
069 * described in section 5.2.1.8 of
070 * <A HREF="http://www.ietf.org/rfc/rfc4513.txt">RFC 4513</A>. That is,
071 * it should be either "dn:" followed by the distinguished name of the
072 * target user, or "u:" followed by the username. If the "u:" form is
073 * used, then the mechanism used to resolve the provided username to an
074 * entry may vary from server to server.</LI>
075 * <LI>Authorization ID -- An optional string which specifies an alternate
076 * authorization identity that should be used for subsequent operations
077 * requested on the connection. Like the authentication ID, the
078 * authorization ID should use the "authzId" syntax.</LI>
079 * <LI>Realm -- An optional string which specifies the realm into which the
080 * user should authenticate.</LI>
081 * <LI>Password -- The clear-text password for the target user.</LI>
082 * </UL>
083 * <H2>Example</H2>
084 * The following example demonstrates the process for performing a DIGEST-MD5
085 * bind against a directory server with a username of "john.doe" and a password
086 * of "password":
087 * <PRE>
088 * DIGESTMD5BindRequest bindRequest =
089 * new DIGESTMD5BindRequest("u:john.doe", "password");
090 * BindResult bindResult;
091 * try
092 * {
093 * bindResult = connection.bind(bindRequest);
094 * // If we get here, then the bind was successful.
095 * }
096 * catch (LDAPException le)
097 * {
098 * // The bind failed for some reason.
099 * bindResult = new BindResult(le.toLDAPResult());
100 * ResultCode resultCode = le.getResultCode();
101 * String errorMessageFromServer = le.getDiagnosticMessage();
102 * }
103 * </PRE>
104 */
105 @NotMutable()
106 @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
107 public final class DIGESTMD5BindRequest
108 extends SASLBindRequest
109 implements CallbackHandler
110 {
111 /**
112 * The name for the DIGEST-MD5 SASL mechanism.
113 */
114 public static final String DIGESTMD5_MECHANISM_NAME = "DIGEST-MD5";
115
116
117
118 /**
119 * The serial version UID for this serializable class.
120 */
121 private static final long serialVersionUID = 867592367640540593L;
122
123
124
125 // The password for this bind request.
126 private final ASN1OctetString password;
127
128 // The message ID from the last LDAP message sent from this request.
129 private int messageID = -1;
130
131 // The SASL quality of protection value(s) allowed for the DIGEST-MD5 bind
132 // request.
133 private final List<SASLQualityOfProtection> allowedQoP;
134
135 // A list that will be updated with messages about any unhandled callbacks
136 // encountered during processing.
137 private final List<String> unhandledCallbackMessages;
138
139 // The authentication ID string for this bind request.
140 private final String authenticationID;
141
142 // The authorization ID string for this bind request, if available.
143 private final String authorizationID;
144
145 // The realm form this bind request, if available.
146 private final String realm;
147
148
149
150 /**
151 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
152 * ID and password. It will not include an authorization ID, a realm, or any
153 * controls.
154 *
155 * @param authenticationID The authentication ID for this bind request. It
156 * must not be {@code null}.
157 * @param password The password for this bind request. It must not
158 * be {@code null}.
159 */
160 public DIGESTMD5BindRequest(final String authenticationID,
161 final String password)
162 {
163 this(authenticationID, null, new ASN1OctetString(password), null,
164 NO_CONTROLS);
165
166 ensureNotNull(password);
167 }
168
169
170
171 /**
172 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
173 * ID and password. It will not include an authorization ID, a realm, or any
174 * controls.
175 *
176 * @param authenticationID The authentication ID for this bind request. It
177 * must not be {@code null}.
178 * @param password The password for this bind request. It must not
179 * be {@code null}.
180 */
181 public DIGESTMD5BindRequest(final String authenticationID,
182 final byte[] password)
183 {
184 this(authenticationID, null, new ASN1OctetString(password), null,
185 NO_CONTROLS);
186
187 ensureNotNull(password);
188 }
189
190
191
192 /**
193 * Creates a new SASL DIGEST-MD5 bind request with the provided authentication
194 * ID and password. It will not include an authorization ID, a realm, or any
195 * controls.
196 *
197 * @param authenticationID The authentication ID for this bind request. It
198 * must not be {@code null}.
199 * @param password The password for this bind request. It must not
200 * be {@code null}.
201 */
202 public DIGESTMD5BindRequest(final String authenticationID,
203 final ASN1OctetString password)
204 {
205 this(authenticationID, null, password, null, NO_CONTROLS);
206 }
207
208
209
210 /**
211 * Creates a new SASL DIGEST-MD5 bind request with the provided information.
212 *
213 * @param authenticationID The authentication ID for this bind request. It
214 * must not be {@code null}.
215 * @param authorizationID The authorization ID for this bind request. It
216 * may be {@code null} if there will not be an
217 * alternate authorization identity.
218 * @param password The password for this bind request. It must not
219 * be {@code null}.
220 * @param realm The realm to use for the authentication. It may
221 * be {@code null} if the server supports a default
222 * realm.
223 * @param controls The set of controls to include in the request.
224 */
225 public DIGESTMD5BindRequest(final String authenticationID,
226 final String authorizationID,
227 final String password, final String realm,
228 final Control... controls)
229 {
230 this(authenticationID, authorizationID, new ASN1OctetString(password),
231 realm, controls);
232
233 ensureNotNull(password);
234 }
235
236
237
238 /**
239 * Creates a new SASL DIGEST-MD5 bind request with the provided information.
240 *
241 * @param authenticationID The authentication ID for this bind request. It
242 * must not be {@code null}.
243 * @param authorizationID The authorization ID for this bind request. It
244 * may be {@code null} if there will not be an
245 * alternate authorization identity.
246 * @param password The password for this bind request. It must not
247 * be {@code null}.
248 * @param realm The realm to use for the authentication. It may
249 * be {@code null} if the server supports a default
250 * realm.
251 * @param controls The set of controls to include in the request.
252 */
253 public DIGESTMD5BindRequest(final String authenticationID,
254 final String authorizationID,
255 final byte[] password, final String realm,
256 final Control... controls)
257 {
258 this(authenticationID, authorizationID, new ASN1OctetString(password),
259 realm, controls);
260
261 ensureNotNull(password);
262 }
263
264
265
266 /**
267 * Creates a new SASL DIGEST-MD5 bind request with the provided information.
268 *
269 * @param authenticationID The authentication ID for this bind request. It
270 * must not be {@code null}.
271 * @param authorizationID The authorization ID for this bind request. It
272 * may be {@code null} if there will not be an
273 * alternate authorization identity.
274 * @param password The password for this bind request. It must not
275 * be {@code null}.
276 * @param realm The realm to use for the authentication. It may
277 * be {@code null} if the server supports a default
278 * realm.
279 * @param controls The set of controls to include in the request.
280 */
281 public DIGESTMD5BindRequest(final String authenticationID,
282 final String authorizationID,
283 final ASN1OctetString password,
284 final String realm, final Control... controls)
285 {
286 super(controls);
287
288 ensureNotNull(authenticationID, password);
289
290 this.authenticationID = authenticationID;
291 this.authorizationID = authorizationID;
292 this.password = password;
293 this.realm = realm;
294
295 allowedQoP = Collections.unmodifiableList(
296 Arrays.asList(SASLQualityOfProtection.AUTH));
297
298 unhandledCallbackMessages = new ArrayList<String>(5);
299 }
300
301
302
303 /**
304 * Creates a new SASL DIGEST-MD5 bind request with the provided set of
305 * properties.
306 *
307 * @param properties The properties to use for this
308 * @param controls The set of controls to include in the request.
309 */
310 public DIGESTMD5BindRequest(final DIGESTMD5BindRequestProperties properties,
311 final Control... controls)
312 {
313 super(controls);
314
315 ensureNotNull(properties);
316
317 authenticationID = properties.getAuthenticationID();
318 authorizationID = properties.getAuthorizationID();
319 password = properties.getPassword();
320 realm = properties.getRealm();
321 allowedQoP = properties.getAllowedQoP();
322
323 unhandledCallbackMessages = new ArrayList<String>(5);
324 }
325
326
327
328 /**
329 * {@inheritDoc}
330 */
331 @Override()
332 public String getSASLMechanismName()
333 {
334 return DIGESTMD5_MECHANISM_NAME;
335 }
336
337
338
339 /**
340 * Retrieves the authentication ID for this bind request.
341 *
342 * @return The authentication ID for this bind request.
343 */
344 public String getAuthenticationID()
345 {
346 return authenticationID;
347 }
348
349
350
351 /**
352 * Retrieves the authorization ID for this bind request, if any.
353 *
354 * @return The authorization ID for this bind request, or {@code null} if
355 * there should not be a separate authorization identity.
356 */
357 public String getAuthorizationID()
358 {
359 return authorizationID;
360 }
361
362
363
364 /**
365 * Retrieves the string representation of the password for this bind request.
366 *
367 * @return The string representation of the password for this bind request.
368 */
369 public String getPasswordString()
370 {
371 return password.stringValue();
372 }
373
374
375
376 /**
377 * Retrieves the bytes that comprise the the password for this bind request.
378 *
379 * @return The bytes that comprise the password for this bind request.
380 */
381 public byte[] getPasswordBytes()
382 {
383 return password.getValue();
384 }
385
386
387
388 /**
389 * Retrieves the realm for this bind request, if any.
390 *
391 * @return The realm for this bind request, or {@code null} if none was
392 * defined and the server should use the default realm.
393 */
394 public String getRealm()
395 {
396 return realm;
397 }
398
399
400
401 /**
402 * Retrieves the list of allowed qualities of protection that may be used for
403 * communication that occurs on the connection after the authentication has
404 * completed, in order from most preferred to least preferred.
405 *
406 * @return The list of allowed qualities of protection that may be used for
407 * communication that occurs on the connection after the
408 * authentication has completed, in order from most preferred to
409 * least preferred.
410 */
411 public List<SASLQualityOfProtection> getAllowedQoP()
412 {
413 return allowedQoP;
414 }
415
416
417
418 /**
419 * Sends this bind request to the target server over the provided connection
420 * and returns the corresponding response.
421 *
422 * @param connection The connection to use to send this bind request to the
423 * server and read the associated response.
424 * @param depth The current referral depth for this request. It should
425 * always be one for the initial request, and should only
426 * be incremented when following referrals.
427 *
428 * @return The bind response read from the server.
429 *
430 * @throws LDAPException If a problem occurs while sending the request or
431 * reading the response.
432 */
433 @Override()
434 protected BindResult process(final LDAPConnection connection, final int depth)
435 throws LDAPException
436 {
437 unhandledCallbackMessages.clear();
438
439 final String[] mechanisms = { DIGESTMD5_MECHANISM_NAME };
440
441 final HashMap<String,Object> saslProperties = new HashMap<String,Object>();
442 saslProperties.put(Sasl.QOP, SASLQualityOfProtection.toString(allowedQoP));
443 saslProperties.put(Sasl.SERVER_AUTH, "false");
444
445 final SaslClient saslClient;
446 try
447 {
448 saslClient = Sasl.createSaslClient(mechanisms, authorizationID, "ldap",
449 connection.getConnectedAddress(),
450 saslProperties, this);
451 }
452 catch (Exception e)
453 {
454 debugException(e);
455 throw new LDAPException(ResultCode.LOCAL_ERROR,
456 ERR_DIGESTMD5_CANNOT_CREATE_SASL_CLIENT.get(getExceptionMessage(e)),
457 e);
458 }
459
460 final SASLHelper helper = new SASLHelper(this, connection,
461 DIGESTMD5_MECHANISM_NAME, saslClient, getControls(),
462 getResponseTimeoutMillis(connection), unhandledCallbackMessages);
463
464 try
465 {
466 return helper.processSASLBind();
467 }
468 finally
469 {
470 messageID = helper.getMessageID();
471 }
472 }
473
474
475
476 /**
477 * {@inheritDoc}
478 */
479 @Override()
480 public DIGESTMD5BindRequest getRebindRequest(final String host,
481 final int port)
482 {
483 final DIGESTMD5BindRequestProperties properties =
484 new DIGESTMD5BindRequestProperties(authenticationID, password);
485 properties.setAuthorizationID(authorizationID);
486 properties.setRealm(realm);
487 properties.setAllowedQoP(allowedQoP);
488
489 return new DIGESTMD5BindRequest(properties, getControls());
490 }
491
492
493
494 /**
495 * Handles any necessary callbacks required for SASL authentication.
496 *
497 * @param callbacks The set of callbacks to be handled.
498 */
499 @InternalUseOnly()
500 public void handle(final Callback[] callbacks)
501 {
502 for (final Callback callback : callbacks)
503 {
504 if (callback instanceof NameCallback)
505 {
506 ((NameCallback) callback).setName(authenticationID);
507 }
508 else if (callback instanceof PasswordCallback)
509 {
510 ((PasswordCallback) callback).setPassword(
511 password.stringValue().toCharArray());
512 }
513 else if (callback instanceof RealmCallback)
514 {
515 final RealmCallback rc = (RealmCallback) callback;
516 if (realm == null)
517 {
518 final String defaultRealm = rc.getDefaultText();
519 if (defaultRealm == null)
520 {
521 unhandledCallbackMessages.add(
522 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get(
523 String.valueOf(rc.getPrompt())));
524 }
525 else
526 {
527 rc.setText(defaultRealm);
528 }
529 }
530 else
531 {
532 rc.setText(realm);
533 }
534 }
535 else if (callback instanceof RealmChoiceCallback)
536 {
537 final RealmChoiceCallback rcc = (RealmChoiceCallback) callback;
538 if (realm == null)
539 {
540 final String choices =
541 concatenateStrings("{", " '", ",", "'", " }", rcc.getChoices());
542 unhandledCallbackMessages.add(
543 ERR_DIGESTMD5_REALM_REQUIRED_BUT_NONE_PROVIDED.get(
544 rcc.getPrompt(), choices));
545 }
546 else
547 {
548 final String[] choices = rcc.getChoices();
549 for (int i=0; i < choices.length; i++)
550 {
551 if (choices[i].equals(realm))
552 {
553 rcc.setSelectedIndex(i);
554 break;
555 }
556 }
557 }
558 }
559 else
560 {
561 // This is an unexpected callback.
562 if (debugEnabled(DebugType.LDAP))
563 {
564 debug(Level.WARNING, DebugType.LDAP,
565 "Unexpected DIGEST-MD5 SASL callback of type " +
566 callback.getClass().getName());
567 }
568
569 unhandledCallbackMessages.add(ERR_DIGESTMD5_UNEXPECTED_CALLBACK.get(
570 callback.getClass().getName()));
571 }
572 }
573 }
574
575
576
577 /**
578 * {@inheritDoc}
579 */
580 @Override()
581 public int getLastMessageID()
582 {
583 return messageID;
584 }
585
586
587
588 /**
589 * {@inheritDoc}
590 */
591 @Override()
592 public DIGESTMD5BindRequest duplicate()
593 {
594 return duplicate(getControls());
595 }
596
597
598
599 /**
600 * {@inheritDoc}
601 */
602 @Override()
603 public DIGESTMD5BindRequest duplicate(final Control[] controls)
604 {
605 final DIGESTMD5BindRequestProperties properties =
606 new DIGESTMD5BindRequestProperties(authenticationID, password);
607 properties.setAuthorizationID(authorizationID);
608 properties.setRealm(realm);
609 properties.setAllowedQoP(allowedQoP);
610
611 final DIGESTMD5BindRequest bindRequest =
612 new DIGESTMD5BindRequest(properties, controls);
613 bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
614 return bindRequest;
615 }
616
617
618
619 /**
620 * {@inheritDoc}
621 */
622 @Override()
623 public void toString(final StringBuilder buffer)
624 {
625 buffer.append("DIGESTMD5BindRequest(authenticationID='");
626 buffer.append(authenticationID);
627 buffer.append('\'');
628
629 if (authorizationID != null)
630 {
631 buffer.append(", authorizationID='");
632 buffer.append(authorizationID);
633 buffer.append('\'');
634 }
635
636 if (realm != null)
637 {
638 buffer.append(", realm='");
639 buffer.append(realm);
640 buffer.append('\'');
641 }
642
643 buffer.append(", qop='");
644 buffer.append(SASLQualityOfProtection.toString(allowedQoP));
645 buffer.append('\'');
646
647 final Control[] controls = getControls();
648 if (controls.length > 0)
649 {
650 buffer.append(", controls={");
651 for (int i=0; i < controls.length; i++)
652 {
653 if (i > 0)
654 {
655 buffer.append(", ");
656 }
657
658 buffer.append(controls[i]);
659 }
660 buffer.append('}');
661 }
662
663 buffer.append(')');
664 }
665
666
667
668 /**
669 * {@inheritDoc}
670 */
671 @Override()
672 public void toCode(final List<String> lineList, final String requestID,
673 final int indentSpaces, final boolean includeProcessing)
674 {
675 // Create and update the bind request properties object.
676 ToCodeHelper.generateMethodCall(lineList, indentSpaces,
677 "DIGESTMD5BindRequestProperties",
678 requestID + "RequestProperties",
679 "new DIGESTMD5BindRequestProperties",
680 ToCodeArgHelper.createString(authenticationID, "Authentication ID"),
681 ToCodeArgHelper.createString("---redacted-password---", "Password"));
682
683 if (authorizationID != null)
684 {
685 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
686 requestID + "RequestProperties.setAuthorizationID",
687 ToCodeArgHelper.createString(authorizationID, null));
688 }
689
690 if (realm != null)
691 {
692 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
693 requestID + "RequestProperties.setRealm",
694 ToCodeArgHelper.createString(realm, null));
695 }
696
697 final ArrayList<String> qopValues = new ArrayList<String>();
698 for (final SASLQualityOfProtection qop : allowedQoP)
699 {
700 qopValues.add("SASLQualityOfProtection." + qop.name());
701 }
702 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
703 requestID + "RequestProperties.setAllowedQoP",
704 ToCodeArgHelper.createRaw(qopValues, null));
705
706
707 // Create the request variable.
708 final ArrayList<ToCodeArgHelper> constructorArgs =
709 new ArrayList<ToCodeArgHelper>(2);
710 constructorArgs.add(
711 ToCodeArgHelper.createRaw(requestID + "RequestProperties", null));
712
713 final Control[] controls = getControls();
714 if (controls.length > 0)
715 {
716 constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
717 "Bind Controls"));
718 }
719
720 ToCodeHelper.generateMethodCall(lineList, indentSpaces,
721 "DIGESTMD5BindRequest", requestID + "Request",
722 "new DIGESTMD5BindRequest", constructorArgs);
723
724
725 // Add lines for processing the request and obtaining the result.
726 if (includeProcessing)
727 {
728 // Generate a string with the appropriate indent.
729 final StringBuilder buffer = new StringBuilder();
730 for (int i=0; i < indentSpaces; i++)
731 {
732 buffer.append(' ');
733 }
734 final String indent = buffer.toString();
735
736 lineList.add("");
737 lineList.add(indent + "try");
738 lineList.add(indent + '{');
739 lineList.add(indent + " BindResult " + requestID +
740 "Result = connection.bind(" + requestID + "Request);");
741 lineList.add(indent + " // The bind was processed successfully.");
742 lineList.add(indent + '}');
743 lineList.add(indent + "catch (LDAPException e)");
744 lineList.add(indent + '{');
745 lineList.add(indent + " // The bind failed. Maybe the following will " +
746 "help explain why.");
747 lineList.add(indent + " // Note that the connection is now likely in " +
748 "an unauthenticated state.");
749 lineList.add(indent + " ResultCode resultCode = e.getResultCode();");
750 lineList.add(indent + " String message = e.getMessage();");
751 lineList.add(indent + " String matchedDN = e.getMatchedDN();");
752 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();");
753 lineList.add(indent + " Control[] responseControls = " +
754 "e.getResponseControls();");
755 lineList.add(indent + '}');
756 }
757 }
758 }