/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2009 Sun Microsystems, Inc. * Portions copyright 2011-2013 ForgeRock AS. */ package org.opends.server.extensions; import static org.opends.messages.CoreMessages.*; import static org.opends.messages.ExtensionMessages.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Lock; import org.opends.messages.Message; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.admin.std.server.PlainSASLMechanismHandlerCfg; import org.opends.server.admin.std.server.SASLMechanismHandlerCfg; import org.opends.server.api.AuthenticationPolicyState; import org.opends.server.api.IdentityMapper; import org.opends.server.api.SASLMechanismHandler; import org.opends.server.config.ConfigException; import org.opends.server.core.BindOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.types.*; /** * This class provides an implementation of a SASL mechanism that uses * plain-text authentication. It is based on the proposal defined in * draft-ietf-sasl-plain-08 in which the SASL credentials are in the form: *
*
[authzid] UTF8NULL authcid UTF8NULL passwd
*
* Note that this is a weak mechanism by itself and does not offer any * protection for the password, so it may need to be used in conjunction with a * connection security provider to prevent exposing the password. */ public class PlainSASLMechanismHandler extends SASLMechanismHandler implements ConfigurationChangeListener< PlainSASLMechanismHandlerCfg> { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** The identity mapper that will be used to map ID strings to user entries.*/ private IdentityMapper identityMapper; /** The current configuration for this SASL mechanism handler. */ private PlainSASLMechanismHandlerCfg currentConfig; /** * Creates a new instance of this SASL mechanism handler. No initialization * should be done in this method, as it should all be performed in the * initializeSASLMechanismHandler method. */ public PlainSASLMechanismHandler() { super(); } /** * {@inheritDoc} */ @Override() public void initializeSASLMechanismHandler( PlainSASLMechanismHandlerCfg configuration) throws ConfigException, InitializationException { configuration.addPlainChangeListener(this); currentConfig = configuration; // Get the identity mapper that should be used to find users. DN identityMapperDN = configuration.getIdentityMapperDN(); identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_PLAIN, this); } /** * {@inheritDoc} */ @Override() public void finalizeSASLMechanismHandler() { currentConfig.removePlainChangeListener(this); DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_PLAIN); } /** * {@inheritDoc} */ @Override() public void processSASLBind(BindOperation bindOperation) { // Get the SASL credentials provided by the user and decode them. String authzID = null; String authcID = null; String password = null; ByteString saslCredentials = bindOperation.getSASLCredentials(); if (saslCredentials == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_NO_SASL_CREDENTIALS.get(); bindOperation.setAuthFailureReason(message); return; } String credString = saslCredentials.toString(); int length = credString.length(); int nullPos1 = credString.indexOf('\u0000'); if (nullPos1 < 0) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_NO_NULLS_IN_CREDENTIALS.get(); bindOperation.setAuthFailureReason(message); return; } if (nullPos1 > 0) { authzID = credString.substring(0, nullPos1); } int nullPos2 = credString.indexOf('\u0000', nullPos1+1); if (nullPos2 < 0) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_NO_SECOND_NULL.get(); bindOperation.setAuthFailureReason(message); return; } if (nullPos2 == (nullPos1+1)) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_ZERO_LENGTH_AUTHCID.get(); bindOperation.setAuthFailureReason(message); return; } if (nullPos2 == (length-1)) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_ZERO_LENGTH_PASSWORD.get(); bindOperation.setAuthFailureReason(message); return; } authcID = credString.substring(nullPos1+1, nullPos2); password = credString.substring(nullPos2+1); // Get the user entry for the authentication ID. Allow for an // authentication ID that is just a username (as per the SASL PLAIN spec), // but also allow a value in the authzid form specified in RFC 2829. Entry userEntry = null; String lowerAuthcID = toLowerCase(authcID); if (lowerAuthcID.startsWith("dn:")) { // Try to decode the user DN and retrieve the corresponding entry. DN userDN; try { userDN = DN.decode(authcID.substring(3)); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_CANNOT_DECODE_AUTHCID_AS_DN.get( authcID, de.getMessageObject()); bindOperation.setAuthFailureReason(message); return; } if (userDN.isNullDN()) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHCID_IS_NULL_DN.get(); bindOperation.setAuthFailureReason(message); return; } DN rootDN = DirectoryServer.getActualRootBindDN(userDN); if (rootDN != null) { userDN = rootDN; } // Acquire a read lock on the user entry. If this fails, then so will the // authentication. final Lock readLock = LockManager.lockRead(userDN); if (readLock == null) { bindOperation.setResultCode(ResultCode.BUSY); bindOperation.setAuthFailureReason(INFO_SASLPLAIN_CANNOT_LOCK_ENTRY .get(String.valueOf(userDN))); return; } try { userEntry = DirectoryServer.getEntry(userDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_CANNOT_GET_ENTRY_BY_DN.get( String.valueOf(userDN), de.getMessageObject()); bindOperation.setAuthFailureReason(message); return; } finally { LockManager.unlock(userDN, readLock); } } else { // Use the identity mapper to resolve the username to an entry. if (lowerAuthcID.startsWith("u:")) { authcID = authcID.substring(2); } try { userEntry = identityMapper.getEntryForID(authcID); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_CANNOT_MAP_USERNAME.get( String.valueOf(authcID), de.getMessageObject()); bindOperation.setAuthFailureReason(message); return; } } // At this point, we should have a user entry. If we don't then fail. if (userEntry == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_NO_MATCHING_ENTRIES.get(authcID); bindOperation.setAuthFailureReason(message); return; } else { bindOperation.setSASLAuthUserEntry(userEntry); } // If an authorization ID was provided, then make sure that it is // acceptable. Entry authZEntry = userEntry; if (authzID != null) { String lowerAuthzID = toLowerCase(authzID); if (lowerAuthzID.startsWith("dn:")) { DN authzDN; try { authzDN = DN.decode(authzID.substring(3)); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHZID_INVALID_DN.get( authzID, de.getMessageObject()); bindOperation.setAuthFailureReason(message); return; } DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN); if (actualAuthzDN != null) { authzDN = actualAuthzDN; } if (! authzDN.equals(userEntry.getDN())) { AuthenticationInfo tempAuthInfo = new AuthenticationInfo(userEntry, DirectoryServer.isRootDN(userEntry.getDN())); InternalClientConnection tempConn = new InternalClientConnection(tempAuthInfo); if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation)) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get( String.valueOf(userEntry.getDN())); bindOperation.setAuthFailureReason(message); return; } if (authzDN.isNullDN()) { authZEntry = null; } else { try { authZEntry = DirectoryServer.getEntry(authzDN); if (authZEntry == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHZID_NO_SUCH_ENTRY.get( String.valueOf(authzDN)); bindOperation.setAuthFailureReason(message); return; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHZID_CANNOT_GET_ENTRY.get( String.valueOf(authzDN), de.getMessageObject()); bindOperation.setAuthFailureReason(message); return; } } } } else { String idStr; if (lowerAuthzID.startsWith("u:")) { idStr = authzID.substring(2); } else { idStr = authzID; } if (idStr.length() == 0) { authZEntry = null; } else { try { authZEntry = identityMapper.getEntryForID(idStr); if (authZEntry == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHZID_NO_MAPPED_ENTRY.get( authzID); bindOperation.setAuthFailureReason(message); return; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHZID_CANNOT_MAP_AUTHZID.get( authzID, de.getMessageObject()); bindOperation.setAuthFailureReason(message); return; } } if ((authZEntry == null) || (! authZEntry.getDN().equals(userEntry.getDN()))) { AuthenticationInfo tempAuthInfo = new AuthenticationInfo(userEntry, DirectoryServer.isRootDN(userEntry.getDN())); InternalClientConnection tempConn = new InternalClientConnection(tempAuthInfo); if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation)) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_AUTHZID_INSUFFICIENT_PRIVILEGES.get( String.valueOf(userEntry.getDN())); bindOperation.setAuthFailureReason(message); return; } } } } // Get the password policy for the user and use it to determine if the // provided password was correct. try { // FIXME: we should store store the auth state in with the bind operation // so that any state updates, such as cached passwords, are persisted to // the user's entry when the bind completes. AuthenticationPolicyState authState = AuthenticationPolicyState.forUser( userEntry, false); if (authState.isDisabled()) { // Check to see if the user is administratively disabled or locked. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_BIND_OPERATION_ACCOUNT_DISABLED.get(); bindOperation.setAuthFailureReason(message); return; } if (!authState.passwordMatches(ByteString.valueOf(password))) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_INVALID_PASSWORD.get(); bindOperation.setAuthFailureReason(message); return; } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); Message message = ERR_SASLPLAIN_CANNOT_CHECK_PASSWORD_VALIDITY.get( String.valueOf(userEntry.getDN()), String.valueOf(e)); bindOperation.setAuthFailureReason(message); return; } // If we've gotten here, then the authentication was successful. bindOperation.setResultCode(ResultCode.SUCCESS); AuthenticationInfo authInfo = new AuthenticationInfo(userEntry, authZEntry, SASL_MECHANISM_PLAIN, bindOperation.getSASLCredentials(), DirectoryServer.isRootDN(userEntry.getDN())); bindOperation.setAuthenticationInfo(authInfo); return; } /** * {@inheritDoc} */ @Override() public boolean isPasswordBased(String mechanism) { // This is a password-based mechanism. return true; } /** * {@inheritDoc} */ @Override() public boolean isSecure(String mechanism) { // This is not a secure mechanism. return false; } /** * {@inheritDoc} */ @Override() public boolean isConfigurationAcceptable( SASLMechanismHandlerCfg configuration, List unacceptableReasons) { PlainSASLMechanismHandlerCfg config = (PlainSASLMechanismHandlerCfg) configuration; return isConfigurationChangeAcceptable(config, unacceptableReasons); } /** * {@inheritDoc} */ @Override public boolean isConfigurationChangeAcceptable( PlainSASLMechanismHandlerCfg configuration, List unacceptableReasons) { return true; } /** * {@inheritDoc} */ @Override public ConfigChangeResult applyConfigurationChange( PlainSASLMechanismHandlerCfg configuration) { ResultCode resultCode = ResultCode.SUCCESS; boolean adminActionRequired = false; ArrayList messages = new ArrayList(); // Get the identity mapper that should be used to find users. DN identityMapperDN = configuration.getIdentityMapperDN(); identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); currentConfig = configuration; return new ConfigChangeResult(resultCode, adminActionRequired, messages); } }