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.extensions;
028 import org.opends.messages.Message;
029
030
031
032 import java.util.ArrayList;
033 import java.util.List;
034 import java.util.concurrent.locks.Lock;
035
036 import org.opends.server.admin.server.ConfigurationChangeListener;
037 import org.opends.server.admin.std.server.PlainSASLMechanismHandlerCfg;
038 import org.opends.server.admin.std.server.SASLMechanismHandlerCfg;
039 import org.opends.server.api.IdentityMapper;
040 import org.opends.server.api.SASLMechanismHandler;
041 import org.opends.server.config.ConfigException;
042 import org.opends.server.core.BindOperation;
043 import org.opends.server.core.DirectoryServer;
044 import org.opends.server.core.PasswordPolicyState;
045 import org.opends.server.protocols.asn1.ASN1OctetString;
046 import org.opends.server.protocols.internal.InternalClientConnection;
047 import org.opends.server.types.AuthenticationInfo;
048 import org.opends.server.types.ByteString;
049 import org.opends.server.types.ConfigChangeResult;
050 import org.opends.server.types.DirectoryException;
051 import org.opends.server.types.DN;
052 import org.opends.server.types.Entry;
053 import org.opends.server.types.InitializationException;
054 import org.opends.server.types.LockManager;
055 import org.opends.server.types.Privilege;
056 import org.opends.server.types.ResultCode;
057
058 import static org.opends.server.loggers.debug.DebugLogger.*;
059 import org.opends.server.loggers.debug.DebugTracer;
060 import org.opends.server.types.DebugLogLevel;
061 import static org.opends.messages.ExtensionMessages.*;
062
063 import static org.opends.server.util.ServerConstants.*;
064 import static org.opends.server.util.StaticUtils.*;
065
066
067
068 /**
069 * This class provides an implementation of a SASL mechanism that uses
070 * plain-text authentication. It is based on the proposal defined in
071 * draft-ietf-sasl-plain-08 in which the SASL credentials are in the form:
072 * <BR>
073 * <BLOCKQUOTE>[authzid] UTF8NULL authcid UTF8NULL passwd</BLOCKQUOTE>
074 * <BR>
075 * Note that this is a weak mechanism by itself and does not offer any
076 * protection for the password, so it may need to be used in conjunction with a
077 * connection security provider to prevent exposing the password.
078 */
079 public class PlainSASLMechanismHandler
080 extends SASLMechanismHandler<PlainSASLMechanismHandlerCfg>
081 implements ConfigurationChangeListener<
082 PlainSASLMechanismHandlerCfg>
083 {
084 /**
085 * The tracer object for the debug logger.
086 */
087 private static final DebugTracer TRACER = getTracer();
088
089 // The identity mapper that will be used to map ID strings to user entries.
090 private IdentityMapper<?> identityMapper;
091
092 // The current configuration for this SASL mechanism handler.
093 private PlainSASLMechanismHandlerCfg currentConfig;
094
095
096
097 /**
098 * Creates a new instance of this SASL mechanism handler. No initialization
099 * should be done in this method, as it should all be performed in the
100 * <CODE>initializeSASLMechanismHandler</CODE> method.
101 */
102 public PlainSASLMechanismHandler()
103 {
104 super();
105 }
106
107
108
109 /**
110 * {@inheritDoc}
111 */
112 @Override()
113 public void initializeSASLMechanismHandler(
114 PlainSASLMechanismHandlerCfg configuration)
115 throws ConfigException, InitializationException
116 {
117 configuration.addPlainChangeListener(this);
118 currentConfig = configuration;
119
120
121 // Get the identity mapper that should be used to find users.
122 DN identityMapperDN = configuration.getIdentityMapperDN();
123 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
124
125
126 DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_PLAIN, this);
127 }
128
129
130
131 /**
132 * {@inheritDoc}
133 */
134 @Override()
135 public void finalizeSASLMechanismHandler()
136 {
137 currentConfig.removePlainChangeListener(this);
138 DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_PLAIN);
139 }
140
141
142
143
144 /**
145 * {@inheritDoc}
146 */
147 @Override()
148 public void processSASLBind(BindOperation bindOperation)
149 {
150 IdentityMapper<?> identityMapper = this.identityMapper;
151
152 // Get the SASL credentials provided by the user and decode them.
153 String authzID = null;
154 String authcID = null;
155 String password = null;
156
157 ByteString saslCredentials = bindOperation.getSASLCredentials();
158 if (saslCredentials == null)
159 {
160 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
161
162 Message message = ERR_SASLPLAIN_NO_SASL_CREDENTIALS.get();
163 bindOperation.setAuthFailureReason(message);
164 return;
165 }
166
167 String credString = saslCredentials.stringValue();
168 int length = credString.length();
169 int nullPos1 = credString.indexOf('\u0000');
170 if (nullPos1 < 0)
171 {
172 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
173
174 Message message = ERR_SASLPLAIN_NO_NULLS_IN_CREDENTIALS.get();
175 bindOperation.setAuthFailureReason(message);
176 return;
177 }
178
179 if (nullPos1 > 0)
180 {
181 authzID = credString.substring(0, nullPos1);
182 }
183
184
185 int nullPos2 = credString.indexOf('\u0000', nullPos1+1);
186 if (nullPos2 < 0)
187 {
188 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
189
190 Message message = ERR_SASLPLAIN_NO_SECOND_NULL.get();
191 bindOperation.setAuthFailureReason(message);
192 return;
193 }
194
195 if (nullPos2 == (nullPos1+1))
196 {
197 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
198
199 Message message = ERR_SASLPLAIN_ZERO_LENGTH_AUTHCID.get();
200 bindOperation.setAuthFailureReason(message);
201 return;
202 }
203
204 if (nullPos2 == (length-1))
205 {
206 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
207
208 Message message = ERR_SASLPLAIN_ZERO_LENGTH_PASSWORD.get();
209 bindOperation.setAuthFailureReason(message);
210 return;
211 }
212
213 authcID = credString.substring(nullPos1+1, nullPos2);
214 password = credString.substring(nullPos2+1);
215
216
217 // Get the user entry for the authentication ID. Allow for an
218 // authentication ID that is just a username (as per the SASL PLAIN spec),
219 // but also allow a value in the authzid form specified in RFC 2829.
220 Entry userEntry = null;
221 String lowerAuthcID = toLowerCase(authcID);
222 if (lowerAuthcID.startsWith("dn:"))
223 {
224 // Try to decode the user DN and retrieve the corresponding entry.
225 DN userDN;
226 try
227 {
228 userDN = DN.decode(authcID.substring(3));
229 }
230 catch (DirectoryException de)
231 {
232 if (debugEnabled())
233 {
234 TRACER.debugCaught(DebugLogLevel.ERROR, de);
235 }
236
237 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
238
239 Message message = ERR_SASLPLAIN_CANNOT_DECODE_AUTHCID_AS_DN.get(
240 authcID, de.getMessageObject());
241 bindOperation.setAuthFailureReason(message);
242 return;
243 }
244
245 if (userDN.isNullDN())
246 {
247 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
248
249 Message message = ERR_SASLPLAIN_AUTHCID_IS_NULL_DN.get();
250 bindOperation.setAuthFailureReason(message);
251 return;
252 }
253
254 DN rootDN = DirectoryServer.getActualRootBindDN(userDN);
255 if (rootDN != null)
256 {
257 userDN = rootDN;
258 }
259
260 // Acquire a read lock on the user entry. If this fails, then so will the
261 // authentication.
262 Lock readLock = null;
263 for (int i=0; i < 3; i++)
264 {
265 readLock = LockManager.lockRead(userDN);
266 if (readLock != null)
267 {
268 break;
269 }
270 }
271
272 if (readLock == null)
273 {
274 bindOperation.setResultCode(DirectoryServer.getServerErrorResultCode());
275
276 Message message = INFO_SASLPLAIN_CANNOT_LOCK_ENTRY.get(String.valueOf(
277 userDN));
278 bindOperation.setAuthFailureReason(message);
279 return;
280 }
281
282 try
283 {
284 userEntry = DirectoryServer.getEntry(userDN);
285 }
286 catch (DirectoryException de)
287 {
288 if (debugEnabled())
289 {
290 TRACER.debugCaught(DebugLogLevel.ERROR, de);
291 }
292
293 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
294
295 Message message = ERR_SASLPLAIN_CANNOT_GET_ENTRY_BY_DN.get(
296 String.valueOf(userDN),
297 de.getMessageObject());
298 bindOperation.setAuthFailureReason(message);
299 return;
300 }
301 finally
302 {
303 LockManager.unlock(userDN, readLock);
304 }
305 }
306 else
307 {
308 // Use the identity mapper to resolve the username to an entry.
309 if (lowerAuthcID.startsWith("u:"))
310 {
311 authcID = authcID.substring(2);
312 }
313
314 try
315 {
316 userEntry = identityMapper.getEntryForID(authcID);
317 }
318 catch (DirectoryException de)
319 {
320 if (debugEnabled())
321 {
322 TRACER.debugCaught(DebugLogLevel.ERROR, de);
323 }
324
325 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
326
327 Message message = ERR_SASLPLAIN_CANNOT_MAP_USERNAME.get(
328 String.valueOf(authcID),
329 de.getMessageObject());
330 bindOperation.setAuthFailureReason(message);
331 return;
332 }
333 }
334
335
336 // At this point, we should have a user entry. If we don't then fail.
337 if (userEntry == null)
338 {
339 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
340
341 Message message = ERR_SASLPLAIN_NO_MATCHING_ENTRIES.get(authcID);
342 bindOperation.setAuthFailureReason(message);
343 return;
344 }
345 else
346 {
347 bindOperation.setSASLAuthUserEntry(userEntry);
348 }
349
350
351 // If an authorization ID was provided, then make sure that it is
352 // acceptable.
353 Entry authZEntry = userEntry;
354 if (authzID != null)
355 {
356 String lowerAuthzID = toLowerCase(authzID);
357 if (lowerAuthzID.startsWith("dn:"))
358 {
359 DN authzDN;
360 try
361 {
362 authzDN = DN.decode(authzID.substring(3));
363 }
364 catch (DirectoryException de)
365 {
366 if (debugEnabled())
367 {
368 TRACER.debugCaught(DebugLogLevel.ERROR, de);
369 }
370
371 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
372
373 Message message = ERR_SASLPLAIN_AUTHZID_INVALID_DN.get(
374 authzID, de.getMessageObject());
375 bindOperation.setAuthFailureReason(message);
376 return;
377 }
378
379 DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN);
380 if (actualAuthzDN != null)
381 {
382 authzDN = actualAuthzDN;
383 }
384
385 if (! authzDN.equals(userEntry.getDN()))
386 {
387 AuthenticationInfo tempAuthInfo =
388 new AuthenticationInfo(userEntry,
389 DirectoryServer.isRootDN(userEntry.getDN()));
390 InternalClientConnection tempConn =
391 new InternalClientConnection(tempAuthInfo);
392 if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
393 {
394 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
395
396 Message message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(
397 String.valueOf(userEntry.getDN()));
398 bindOperation.setAuthFailureReason(message);
399 return;
400 }
401
402 if (authzDN.isNullDN())
403 {
404 authZEntry = null;
405 }
406 else
407 {
408 try
409 {
410 authZEntry = DirectoryServer.getEntry(authzDN);
411 if (authZEntry == null)
412 {
413 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
414
415 Message message = ERR_SASLPLAIN_AUTHZID_NO_SUCH_ENTRY.get(
416 String.valueOf(authzDN));
417 bindOperation.setAuthFailureReason(message);
418 return;
419 }
420 }
421 catch (DirectoryException de)
422 {
423 if (debugEnabled())
424 {
425 TRACER.debugCaught(DebugLogLevel.ERROR, de);
426 }
427
428 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
429
430 Message message = ERR_SASLPLAIN_AUTHZID_CANNOT_GET_ENTRY.get(
431 String.valueOf(authzDN),
432 de.getMessageObject());
433 bindOperation.setAuthFailureReason(message);
434 return;
435 }
436 }
437 }
438 }
439 else
440 {
441 String idStr;
442 if (lowerAuthzID.startsWith("u:"))
443 {
444 idStr = authzID.substring(2);
445 }
446 else
447 {
448 idStr = authzID;
449 }
450
451 if (idStr.length() == 0)
452 {
453 authZEntry = null;
454 }
455 else
456 {
457 try
458 {
459 authZEntry = identityMapper.getEntryForID(idStr);
460 if (authZEntry == null)
461 {
462 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
463
464 Message message = ERR_SASLPLAIN_AUTHZID_NO_MAPPED_ENTRY.get(
465 authzID);
466 bindOperation.setAuthFailureReason(message);
467 return;
468 }
469 }
470 catch (DirectoryException de)
471 {
472 if (debugEnabled())
473 {
474 TRACER.debugCaught(DebugLogLevel.ERROR, de);
475 }
476
477 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
478
479 Message message = ERR_SASLPLAIN_AUTHZID_CANNOT_MAP_AUTHZID.get(
480 authzID, de.getMessageObject());
481 bindOperation.setAuthFailureReason(message);
482 return;
483 }
484 }
485
486 if ((authZEntry == null) ||
487 (! authZEntry.getDN().equals(userEntry.getDN())))
488 {
489 AuthenticationInfo tempAuthInfo =
490 new AuthenticationInfo(userEntry,
491 DirectoryServer.isRootDN(userEntry.getDN()));
492 InternalClientConnection tempConn =
493 new InternalClientConnection(tempAuthInfo);
494 if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
495 {
496 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
497
498 Message message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(
499 String.valueOf(userEntry.getDN()));
500 bindOperation.setAuthFailureReason(message);
501 return;
502 }
503 }
504 }
505 }
506
507
508 // Get the password policy for the user and use it to determine if the
509 // provided password was correct.
510 try
511 {
512 PasswordPolicyState pwPolicyState =
513 new PasswordPolicyState(userEntry, false);
514 if (! pwPolicyState.passwordMatches(new ASN1OctetString(password)))
515 {
516 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
517
518 Message message = ERR_SASLPLAIN_INVALID_PASSWORD.get();
519 bindOperation.setAuthFailureReason(message);
520 return;
521 }
522 }
523 catch (Exception e)
524 {
525 if (debugEnabled())
526 {
527 TRACER.debugCaught(DebugLogLevel.ERROR, e);
528 }
529
530 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
531
532 Message message = ERR_SASLPLAIN_CANNOT_CHECK_PASSWORD_VALIDITY.get(
533 String.valueOf(userEntry.getDN()),
534 String.valueOf(e));
535 bindOperation.setAuthFailureReason(message);
536 return;
537 }
538
539
540 // If we've gotten here, then the authentication was successful.
541 bindOperation.setResultCode(ResultCode.SUCCESS);
542
543 AuthenticationInfo authInfo =
544 new AuthenticationInfo(userEntry, authZEntry, SASL_MECHANISM_PLAIN,
545 DirectoryServer.isRootDN(userEntry.getDN()));
546 bindOperation.setAuthenticationInfo(authInfo);
547 return;
548 }
549
550
551
552 /**
553 * {@inheritDoc}
554 */
555 @Override()
556 public boolean isPasswordBased(String mechanism)
557 {
558 // This is a password-based mechanism.
559 return true;
560 }
561
562
563
564 /**
565 * {@inheritDoc}
566 */
567 @Override()
568 public boolean isSecure(String mechanism)
569 {
570 // This is not a secure mechanism.
571 return false;
572 }
573
574
575
576 /**
577 * {@inheritDoc}
578 */
579 @Override()
580 public boolean isConfigurationAcceptable(
581 SASLMechanismHandlerCfg configuration,
582 List<Message> unacceptableReasons)
583 {
584 PlainSASLMechanismHandlerCfg config =
585 (PlainSASLMechanismHandlerCfg) configuration;
586 return isConfigurationChangeAcceptable(config, unacceptableReasons);
587 }
588
589
590
591 /**
592 * {@inheritDoc}
593 */
594 public boolean isConfigurationChangeAcceptable(
595 PlainSASLMechanismHandlerCfg configuration,
596 List<Message> unacceptableReasons)
597 {
598 return true;
599 }
600
601
602
603 /**
604 * {@inheritDoc}
605 */
606 public ConfigChangeResult applyConfigurationChange(
607 PlainSASLMechanismHandlerCfg configuration)
608 {
609 ResultCode resultCode = ResultCode.SUCCESS;
610 boolean adminActionRequired = false;
611 ArrayList<Message> messages = new ArrayList<Message>();
612
613
614 // Get the identity mapper that should be used to find users.
615 DN identityMapperDN = configuration.getIdentityMapperDN();
616 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
617 currentConfig = configuration;
618
619
620 return new ConfigChangeResult(resultCode, adminActionRequired, messages);
621 }
622 }
623