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