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 legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2006-2009 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS. 026 */ 027package org.opends.server.extensions; 028 029import static org.opends.messages.CoreMessages.*; 030import static org.opends.messages.ExtensionMessages.*; 031import static org.opends.server.util.ServerConstants.*; 032import static org.opends.server.util.StaticUtils.*; 033 034import java.util.List; 035import org.forgerock.i18n.LocalizableMessage; 036import org.opends.server.admin.server.ConfigurationChangeListener; 037import org.opends.server.admin.std.server.PlainSASLMechanismHandlerCfg; 038import org.opends.server.admin.std.server.SASLMechanismHandlerCfg; 039import org.opends.server.api.AuthenticationPolicyState; 040import org.opends.server.api.IdentityMapper; 041import org.opends.server.api.SASLMechanismHandler; 042import org.forgerock.opendj.config.server.ConfigChangeResult; 043import org.forgerock.opendj.config.server.ConfigException; 044import org.opends.server.core.BindOperation; 045import org.opends.server.core.DirectoryServer; 046import org.forgerock.i18n.slf4j.LocalizedLogger; 047import org.opends.server.protocols.internal.InternalClientConnection; 048import org.opends.server.types.*; 049import org.forgerock.opendj.ldap.ResultCode; 050import org.forgerock.opendj.ldap.ByteString; 051 052/** 053 * This class provides an implementation of a SASL mechanism that uses 054 * plain-text authentication. It is based on the proposal defined in 055 * draft-ietf-sasl-plain-08 in which the SASL credentials are in the form: 056 * <BR> 057 * <BLOCKQUOTE>[authzid] UTF8NULL authcid UTF8NULL passwd</BLOCKQUOTE> 058 * <BR> 059 * Note that this is a weak mechanism by itself and does not offer any 060 * protection for the password, so it may need to be used in conjunction with a 061 * connection security provider to prevent exposing the password. 062 */ 063public class PlainSASLMechanismHandler 064 extends SASLMechanismHandler<PlainSASLMechanismHandlerCfg> 065 implements ConfigurationChangeListener< 066 PlainSASLMechanismHandlerCfg> 067{ 068 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 069 070 /** The identity mapper that will be used to map ID strings to user entries.*/ 071 private IdentityMapper<?> identityMapper; 072 073 /** The current configuration for this SASL mechanism handler. */ 074 private PlainSASLMechanismHandlerCfg currentConfig; 075 076 077 078 /** 079 * Creates a new instance of this SASL mechanism handler. No initialization 080 * should be done in this method, as it should all be performed in the 081 * <CODE>initializeSASLMechanismHandler</CODE> method. 082 */ 083 public PlainSASLMechanismHandler() 084 { 085 super(); 086 } 087 088 089 090 /** {@inheritDoc} */ 091 @Override 092 public void initializeSASLMechanismHandler( 093 PlainSASLMechanismHandlerCfg configuration) 094 throws ConfigException, InitializationException 095 { 096 configuration.addPlainChangeListener(this); 097 currentConfig = configuration; 098 099 100 // Get the identity mapper that should be used to find users. 101 DN identityMapperDN = configuration.getIdentityMapperDN(); 102 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 103 104 105 DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_PLAIN, this); 106 } 107 108 109 110 /** {@inheritDoc} */ 111 @Override 112 public void finalizeSASLMechanismHandler() 113 { 114 currentConfig.removePlainChangeListener(this); 115 DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_PLAIN); 116 } 117 118 119 120 121 /** {@inheritDoc} */ 122 @Override 123 public void processSASLBind(BindOperation bindOperation) 124 { 125 // Get the SASL credentials provided by the user and decode them. 126 String authzID = null; 127 String authcID = null; 128 String password = null; 129 130 ByteString saslCredentials = bindOperation.getSASLCredentials(); 131 if (saslCredentials == null) 132 { 133 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 134 135 LocalizableMessage message = ERR_SASLPLAIN_NO_SASL_CREDENTIALS.get(); 136 bindOperation.setAuthFailureReason(message); 137 return; 138 } 139 140 String credString = saslCredentials.toString(); 141 int length = credString.length(); 142 int nullPos1 = credString.indexOf('\u0000'); 143 if (nullPos1 < 0) 144 { 145 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 146 147 LocalizableMessage message = ERR_SASLPLAIN_NO_NULLS_IN_CREDENTIALS.get(); 148 bindOperation.setAuthFailureReason(message); 149 return; 150 } 151 152 if (nullPos1 > 0) 153 { 154 authzID = credString.substring(0, nullPos1); 155 } 156 157 158 int nullPos2 = credString.indexOf('\u0000', nullPos1+1); 159 if (nullPos2 < 0) 160 { 161 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 162 163 LocalizableMessage message = ERR_SASLPLAIN_NO_SECOND_NULL.get(); 164 bindOperation.setAuthFailureReason(message); 165 return; 166 } 167 168 if (nullPos2 == (nullPos1+1)) 169 { 170 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 171 172 LocalizableMessage message = ERR_SASLPLAIN_ZERO_LENGTH_AUTHCID.get(); 173 bindOperation.setAuthFailureReason(message); 174 return; 175 } 176 177 if (nullPos2 == (length-1)) 178 { 179 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 180 181 LocalizableMessage message = ERR_SASLPLAIN_ZERO_LENGTH_PASSWORD.get(); 182 bindOperation.setAuthFailureReason(message); 183 return; 184 } 185 186 authcID = credString.substring(nullPos1+1, nullPos2); 187 password = credString.substring(nullPos2+1); 188 189 190 // Get the user entry for the authentication ID. Allow for an 191 // authentication ID that is just a username (as per the SASL PLAIN spec), 192 // but also allow a value in the authzid form specified in RFC 2829. 193 Entry userEntry = null; 194 String lowerAuthcID = toLowerCase(authcID); 195 if (lowerAuthcID.startsWith("dn:")) 196 { 197 // Try to decode the user DN and retrieve the corresponding entry. 198 DN userDN; 199 try 200 { 201 userDN = DN.valueOf(authcID.substring(3)); 202 } 203 catch (DirectoryException de) 204 { 205 logger.traceException(de); 206 207 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 208 209 LocalizableMessage message = ERR_SASLPLAIN_CANNOT_DECODE_AUTHCID_AS_DN.get( 210 authcID, de.getMessageObject()); 211 bindOperation.setAuthFailureReason(message); 212 return; 213 } 214 215 if (userDN.isRootDN()) 216 { 217 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 218 219 LocalizableMessage message = ERR_SASLPLAIN_AUTHCID_IS_NULL_DN.get(); 220 bindOperation.setAuthFailureReason(message); 221 return; 222 } 223 224 DN rootDN = DirectoryServer.getActualRootBindDN(userDN); 225 if (rootDN != null) 226 { 227 userDN = rootDN; 228 } 229 230 try 231 { 232 userEntry = DirectoryServer.getEntry(userDN); 233 } 234 catch (DirectoryException de) 235 { 236 logger.traceException(de); 237 238 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 239 240 LocalizableMessage message = ERR_SASLPLAIN_CANNOT_GET_ENTRY_BY_DN.get(userDN, de.getMessageObject()); 241 bindOperation.setAuthFailureReason(message); 242 return; 243 } 244 } 245 else 246 { 247 // Use the identity mapper to resolve the username to an entry. 248 if (lowerAuthcID.startsWith("u:")) 249 { 250 authcID = authcID.substring(2); 251 } 252 253 try 254 { 255 userEntry = identityMapper.getEntryForID(authcID); 256 } 257 catch (DirectoryException de) 258 { 259 logger.traceException(de); 260 261 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 262 263 LocalizableMessage message = ERR_SASLPLAIN_CANNOT_MAP_USERNAME.get(authcID, de.getMessageObject()); 264 bindOperation.setAuthFailureReason(message); 265 return; 266 } 267 } 268 269 270 // At this point, we should have a user entry. If we don't then fail. 271 if (userEntry == null) 272 { 273 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 274 275 LocalizableMessage message = ERR_SASLPLAIN_NO_MATCHING_ENTRIES.get(authcID); 276 bindOperation.setAuthFailureReason(message); 277 return; 278 } 279 else 280 { 281 bindOperation.setSASLAuthUserEntry(userEntry); 282 } 283 284 285 // If an authorization ID was provided, then make sure that it is 286 // acceptable. 287 Entry authZEntry = userEntry; 288 if (authzID != null) 289 { 290 String lowerAuthzID = toLowerCase(authzID); 291 if (lowerAuthzID.startsWith("dn:")) 292 { 293 DN authzDN; 294 try 295 { 296 authzDN = DN.valueOf(authzID.substring(3)); 297 } 298 catch (DirectoryException de) 299 { 300 logger.traceException(de); 301 302 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 303 304 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INVALID_DN.get( 305 authzID, de.getMessageObject()); 306 bindOperation.setAuthFailureReason(message); 307 return; 308 } 309 310 DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN); 311 if (actualAuthzDN != null) 312 { 313 authzDN = actualAuthzDN; 314 } 315 316 if (! authzDN.equals(userEntry.getName())) 317 { 318 AuthenticationInfo tempAuthInfo = 319 new AuthenticationInfo(userEntry, 320 DirectoryServer.isRootDN(userEntry.getName())); 321 InternalClientConnection tempConn = 322 new InternalClientConnection(tempAuthInfo); 323 if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation)) 324 { 325 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 326 327 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(userEntry.getName()); 328 bindOperation.setAuthFailureReason(message); 329 return; 330 } 331 332 if (authzDN.isRootDN()) 333 { 334 authZEntry = null; 335 } 336 else 337 { 338 try 339 { 340 authZEntry = DirectoryServer.getEntry(authzDN); 341 if (authZEntry == null) 342 { 343 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 344 345 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_NO_SUCH_ENTRY.get(authzDN); 346 bindOperation.setAuthFailureReason(message); 347 return; 348 } 349 } 350 catch (DirectoryException de) 351 { 352 logger.traceException(de); 353 354 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 355 356 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_CANNOT_GET_ENTRY.get(authzDN, de.getMessageObject()); 357 bindOperation.setAuthFailureReason(message); 358 return; 359 } 360 } 361 } 362 } 363 else 364 { 365 String idStr; 366 if (lowerAuthzID.startsWith("u:")) 367 { 368 idStr = authzID.substring(2); 369 } 370 else 371 { 372 idStr = authzID; 373 } 374 375 if (idStr.length() == 0) 376 { 377 authZEntry = null; 378 } 379 else 380 { 381 try 382 { 383 authZEntry = identityMapper.getEntryForID(idStr); 384 if (authZEntry == null) 385 { 386 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 387 388 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_NO_MAPPED_ENTRY.get( 389 authzID); 390 bindOperation.setAuthFailureReason(message); 391 return; 392 } 393 } 394 catch (DirectoryException de) 395 { 396 logger.traceException(de); 397 398 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 399 400 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_CANNOT_MAP_AUTHZID.get( 401 authzID, de.getMessageObject()); 402 bindOperation.setAuthFailureReason(message); 403 return; 404 } 405 } 406 407 if (authZEntry == null || !authZEntry.getName().equals(userEntry.getName())) 408 { 409 AuthenticationInfo tempAuthInfo = 410 new AuthenticationInfo(userEntry, 411 DirectoryServer.isRootDN(userEntry.getName())); 412 InternalClientConnection tempConn = 413 new InternalClientConnection(tempAuthInfo); 414 if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation)) 415 { 416 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 417 418 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(userEntry.getName()); 419 bindOperation.setAuthFailureReason(message); 420 return; 421 } 422 } 423 } 424 } 425 426 427 // Get the password policy for the user and use it to determine if the 428 // provided password was correct. 429 try 430 { 431 // FIXME: we should store store the auth state in with the bind operation 432 // so that any state updates, such as cached passwords, are persisted to 433 // the user's entry when the bind completes. 434 AuthenticationPolicyState authState = AuthenticationPolicyState.forUser( 435 userEntry, false); 436 437 if (authState.isDisabled()) 438 { 439 // Check to see if the user is administratively disabled or locked. 440 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 441 LocalizableMessage message = ERR_BIND_OPERATION_ACCOUNT_DISABLED.get(); 442 bindOperation.setAuthFailureReason(message); 443 return; 444 } 445 446 if (!authState.passwordMatches(ByteString.valueOfUtf8(password))) 447 { 448 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 449 LocalizableMessage message = ERR_SASLPLAIN_INVALID_PASSWORD.get(); 450 bindOperation.setAuthFailureReason(message); 451 return; 452 } 453 } 454 catch (Exception e) 455 { 456 logger.traceException(e); 457 458 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 459 460 LocalizableMessage message = ERR_SASLPLAIN_CANNOT_CHECK_PASSWORD_VALIDITY.get(userEntry.getName(), e); 461 bindOperation.setAuthFailureReason(message); 462 return; 463 } 464 465 466 // If we've gotten here, then the authentication was successful. 467 bindOperation.setResultCode(ResultCode.SUCCESS); 468 469 AuthenticationInfo authInfo = 470 new AuthenticationInfo(userEntry, authZEntry, SASL_MECHANISM_PLAIN, 471 bindOperation.getSASLCredentials(), 472 DirectoryServer.isRootDN(userEntry.getName())); 473 bindOperation.setAuthenticationInfo(authInfo); 474 return; 475 } 476 477 478 479 /** {@inheritDoc} */ 480 @Override 481 public boolean isPasswordBased(String mechanism) 482 { 483 // This is a password-based mechanism. 484 return true; 485 } 486 487 488 489 /** {@inheritDoc} */ 490 @Override 491 public boolean isSecure(String mechanism) 492 { 493 // This is not a secure mechanism. 494 return false; 495 } 496 497 498 499 /** {@inheritDoc} */ 500 @Override 501 public boolean isConfigurationAcceptable( 502 SASLMechanismHandlerCfg configuration, 503 List<LocalizableMessage> unacceptableReasons) 504 { 505 PlainSASLMechanismHandlerCfg config = 506 (PlainSASLMechanismHandlerCfg) configuration; 507 return isConfigurationChangeAcceptable(config, unacceptableReasons); 508 } 509 510 511 512 /** {@inheritDoc} */ 513 @Override 514 public boolean isConfigurationChangeAcceptable( 515 PlainSASLMechanismHandlerCfg configuration, 516 List<LocalizableMessage> unacceptableReasons) 517 { 518 return true; 519 } 520 521 522 523 /** {@inheritDoc} */ 524 @Override 525 public ConfigChangeResult applyConfigurationChange( 526 PlainSASLMechanismHandlerCfg configuration) 527 { 528 final ConfigChangeResult ccr = new ConfigChangeResult(); 529 530 // Get the identity mapper that should be used to find users. 531 DN identityMapperDN = configuration.getIdentityMapperDN(); 532 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 533 currentConfig = configuration; 534 535 return ccr; 536 } 537}