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.forgerock.opendj.config.server.ConfigurationChangeListener; 035import org.forgerock.opendj.server.config.server.PlainSASLMechanismHandlerCfg; 036import org.forgerock.opendj.server.config.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 * Creates a new instance of this SASL mechanism handler. No initialization 075 * should be done in this method, as it should all be performed in the 076 * <CODE>initializeSASLMechanismHandler</CODE> method. 077 */ 078 public PlainSASLMechanismHandler() 079 { 080 super(); 081 } 082 083 @Override 084 public void initializeSASLMechanismHandler( 085 PlainSASLMechanismHandlerCfg configuration) 086 throws ConfigException, InitializationException 087 { 088 configuration.addPlainChangeListener(this); 089 currentConfig = configuration; 090 091 // Get the identity mapper that should be used to find users. 092 DN identityMapperDN = configuration.getIdentityMapperDN(); 093 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 094 095 DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_PLAIN, this); 096 } 097 098 @Override 099 public void finalizeSASLMechanismHandler() 100 { 101 currentConfig.removePlainChangeListener(this); 102 DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_PLAIN); 103 } 104 105 @Override 106 public void processSASLBind(BindOperation bindOperation) 107 { 108 // Get the SASL credentials provided by the user and decode them. 109 String authzID = null; 110 String authcID = null; 111 String password = null; 112 113 ByteString saslCredentials = bindOperation.getSASLCredentials(); 114 if (saslCredentials == null) 115 { 116 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 117 118 LocalizableMessage message = ERR_SASLPLAIN_NO_SASL_CREDENTIALS.get(); 119 bindOperation.setAuthFailureReason(message); 120 return; 121 } 122 123 String credString = saslCredentials.toString(); 124 int length = credString.length(); 125 int nullPos1 = credString.indexOf('\u0000'); 126 if (nullPos1 < 0) 127 { 128 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 129 130 LocalizableMessage message = ERR_SASLPLAIN_NO_NULLS_IN_CREDENTIALS.get(); 131 bindOperation.setAuthFailureReason(message); 132 return; 133 } 134 135 if (nullPos1 > 0) 136 { 137 authzID = credString.substring(0, nullPos1); 138 } 139 140 int nullPos2 = credString.indexOf('\u0000', nullPos1+1); 141 if (nullPos2 < 0) 142 { 143 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 144 145 LocalizableMessage message = ERR_SASLPLAIN_NO_SECOND_NULL.get(); 146 bindOperation.setAuthFailureReason(message); 147 return; 148 } 149 150 if (nullPos2 == (nullPos1+1)) 151 { 152 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 153 154 LocalizableMessage message = ERR_SASLPLAIN_ZERO_LENGTH_AUTHCID.get(); 155 bindOperation.setAuthFailureReason(message); 156 return; 157 } 158 159 if (nullPos2 == (length-1)) 160 { 161 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 162 163 LocalizableMessage message = ERR_SASLPLAIN_ZERO_LENGTH_PASSWORD.get(); 164 bindOperation.setAuthFailureReason(message); 165 return; 166 } 167 168 authcID = credString.substring(nullPos1+1, nullPos2); 169 password = credString.substring(nullPos2+1); 170 171 // Get the user entry for the authentication ID. Allow for an 172 // authentication ID that is just a username (as per the SASL PLAIN spec), 173 // but also allow a value in the authzid form specified in RFC 2829. 174 Entry userEntry = null; 175 String lowerAuthcID = toLowerCase(authcID); 176 if (lowerAuthcID.startsWith("dn:")) 177 { 178 // Try to decode the user DN and retrieve the corresponding entry. 179 DN userDN; 180 try 181 { 182 userDN = DN.valueOf(authcID.substring(3)); 183 } 184 catch (LocalizedIllegalArgumentException e) 185 { 186 logger.traceException(e); 187 188 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 189 bindOperation.setAuthFailureReason( 190 ERR_SASLPLAIN_CANNOT_DECODE_AUTHCID_AS_DN.get(authcID, e.getMessageObject())); 191 return; 192 } 193 194 if (userDN.isRootDN()) 195 { 196 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 197 bindOperation.setAuthFailureReason(ERR_SASLPLAIN_AUTHCID_IS_NULL_DN.get()); 198 return; 199 } 200 201 DN rootDN = DirectoryServer.getActualRootBindDN(userDN); 202 if (rootDN != null) 203 { 204 userDN = rootDN; 205 } 206 207 try 208 { 209 userEntry = DirectoryServer.getEntry(userDN); 210 } 211 catch (DirectoryException de) 212 { 213 logger.traceException(de); 214 215 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 216 217 LocalizableMessage message = ERR_SASLPLAIN_CANNOT_GET_ENTRY_BY_DN.get(userDN, de.getMessageObject()); 218 bindOperation.setAuthFailureReason(message); 219 return; 220 } 221 } 222 else 223 { 224 // Use the identity mapper to resolve the username to an entry. 225 if (lowerAuthcID.startsWith("u:")) 226 { 227 authcID = authcID.substring(2); 228 } 229 230 try 231 { 232 userEntry = identityMapper.getEntryForID(authcID); 233 } 234 catch (DirectoryException de) 235 { 236 logger.traceException(de); 237 238 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 239 240 LocalizableMessage message = ERR_SASLPLAIN_CANNOT_MAP_USERNAME.get(authcID, de.getMessageObject()); 241 bindOperation.setAuthFailureReason(message); 242 return; 243 } 244 } 245 246 // At this point, we should have a user entry. If we don't then fail. 247 if (userEntry == null) 248 { 249 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 250 251 LocalizableMessage message = ERR_SASLPLAIN_NO_MATCHING_ENTRIES.get(authcID); 252 bindOperation.setAuthFailureReason(message); 253 return; 254 } 255 else 256 { 257 bindOperation.setSASLAuthUserEntry(userEntry); 258 } 259 260 // If an authorization ID was provided, then make sure that it is 261 // acceptable. 262 Entry authZEntry = userEntry; 263 if (authzID != null) 264 { 265 String lowerAuthzID = toLowerCase(authzID); 266 if (lowerAuthzID.startsWith("dn:")) 267 { 268 DN authzDN; 269 try 270 { 271 authzDN = DN.valueOf(authzID.substring(3)); 272 } 273 catch (LocalizedIllegalArgumentException e) 274 { 275 logger.traceException(e); 276 277 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 278 bindOperation.setAuthFailureReason(ERR_SASLPLAIN_AUTHZID_INVALID_DN.get(authzID, e.getMessageObject())); 279 return; 280 } 281 282 DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN); 283 if (actualAuthzDN != null) 284 { 285 authzDN = actualAuthzDN; 286 } 287 288 if (! authzDN.equals(userEntry.getName())) 289 { 290 AuthenticationInfo tempAuthInfo = 291 new AuthenticationInfo(userEntry, 292 DirectoryServer.isRootDN(userEntry.getName())); 293 InternalClientConnection tempConn = 294 new InternalClientConnection(tempAuthInfo); 295 if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation)) 296 { 297 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 298 299 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(userEntry.getName()); 300 bindOperation.setAuthFailureReason(message); 301 return; 302 } 303 304 if (authzDN.isRootDN()) 305 { 306 authZEntry = null; 307 } 308 else 309 { 310 try 311 { 312 authZEntry = DirectoryServer.getEntry(authzDN); 313 if (authZEntry == null) 314 { 315 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 316 317 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_NO_SUCH_ENTRY.get(authzDN); 318 bindOperation.setAuthFailureReason(message); 319 return; 320 } 321 } 322 catch (DirectoryException de) 323 { 324 logger.traceException(de); 325 326 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 327 328 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_CANNOT_GET_ENTRY.get(authzDN, de.getMessageObject()); 329 bindOperation.setAuthFailureReason(message); 330 return; 331 } 332 } 333 } 334 } 335 else 336 { 337 String idStr; 338 if (lowerAuthzID.startsWith("u:")) 339 { 340 idStr = authzID.substring(2); 341 } 342 else 343 { 344 idStr = authzID; 345 } 346 347 if (idStr.length() == 0) 348 { 349 authZEntry = null; 350 } 351 else 352 { 353 try 354 { 355 authZEntry = identityMapper.getEntryForID(idStr); 356 if (authZEntry == null) 357 { 358 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 359 360 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_NO_MAPPED_ENTRY.get( 361 authzID); 362 bindOperation.setAuthFailureReason(message); 363 return; 364 } 365 } 366 catch (DirectoryException de) 367 { 368 logger.traceException(de); 369 370 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 371 372 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_CANNOT_MAP_AUTHZID.get( 373 authzID, de.getMessageObject()); 374 bindOperation.setAuthFailureReason(message); 375 return; 376 } 377 } 378 379 if (authZEntry == null || !authZEntry.getName().equals(userEntry.getName())) 380 { 381 AuthenticationInfo tempAuthInfo = 382 new AuthenticationInfo(userEntry, 383 DirectoryServer.isRootDN(userEntry.getName())); 384 InternalClientConnection tempConn = 385 new InternalClientConnection(tempAuthInfo); 386 if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation)) 387 { 388 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 389 390 LocalizableMessage message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get(userEntry.getName()); 391 bindOperation.setAuthFailureReason(message); 392 return; 393 } 394 } 395 } 396 } 397 398 // Get the password policy for the user and use it to determine if the 399 // provided password was correct. 400 try 401 { 402 // FIXME: we should store store the auth state in with the bind operation 403 // so that any state updates, such as cached passwords, are persisted to 404 // the user's entry when the bind completes. 405 AuthenticationPolicyState authState = AuthenticationPolicyState.forUser( 406 userEntry, false); 407 408 if (authState.isDisabled()) 409 { 410 // Check to see if the user is administratively disabled or locked. 411 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 412 LocalizableMessage message = ERR_BIND_OPERATION_ACCOUNT_DISABLED.get(); 413 bindOperation.setAuthFailureReason(message); 414 return; 415 } 416 417 if (!authState.passwordMatches(ByteString.valueOfUtf8(password))) 418 { 419 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 420 LocalizableMessage message = ERR_SASLPLAIN_INVALID_PASSWORD.get(); 421 bindOperation.setAuthFailureReason(message); 422 return; 423 } 424 } 425 catch (Exception e) 426 { 427 logger.traceException(e); 428 429 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 430 431 LocalizableMessage message = ERR_SASLPLAIN_CANNOT_CHECK_PASSWORD_VALIDITY.get(userEntry.getName(), e); 432 bindOperation.setAuthFailureReason(message); 433 return; 434 } 435 436 // If we've gotten here, then the authentication was successful. 437 bindOperation.setResultCode(ResultCode.SUCCESS); 438 439 AuthenticationInfo authInfo = 440 new AuthenticationInfo(userEntry, authZEntry, SASL_MECHANISM_PLAIN, 441 bindOperation.getSASLCredentials(), 442 DirectoryServer.isRootDN(userEntry.getName())); 443 bindOperation.setAuthenticationInfo(authInfo); 444 return; 445 } 446 447 @Override 448 public boolean isPasswordBased(String mechanism) 449 { 450 // This is a password-based mechanism. 451 return true; 452 } 453 454 @Override 455 public boolean isSecure(String mechanism) 456 { 457 // This is not a secure mechanism. 458 return false; 459 } 460 461 @Override 462 public boolean isConfigurationAcceptable( 463 SASLMechanismHandlerCfg configuration, 464 List<LocalizableMessage> unacceptableReasons) 465 { 466 PlainSASLMechanismHandlerCfg config = 467 (PlainSASLMechanismHandlerCfg) configuration; 468 return isConfigurationChangeAcceptable(config, unacceptableReasons); 469 } 470 471 @Override 472 public boolean isConfigurationChangeAcceptable( 473 PlainSASLMechanismHandlerCfg configuration, 474 List<LocalizableMessage> unacceptableReasons) 475 { 476 return true; 477 } 478 479 @Override 480 public ConfigChangeResult applyConfigurationChange( 481 PlainSASLMechanismHandlerCfg configuration) 482 { 483 final ConfigChangeResult ccr = new ConfigChangeResult(); 484 485 // Get the identity mapper that should be used to find users. 486 DN identityMapperDN = configuration.getIdentityMapperDN(); 487 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 488 currentConfig = configuration; 489 490 return ccr; 491 } 492}