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