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