/* * 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 * * * Portions Copyright 2006-2007 Sun Microsystems, Inc. */ package org.opends.server.extensions; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.SecureRandom; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.opends.server.api.Backend; import org.opends.server.api.ClientConnection; import org.opends.server.api.ConfigurableComponent; import org.opends.server.api.IdentityMapper; import org.opends.server.api.SASLMechanismHandler; import org.opends.server.config.ConfigAttribute; import org.opends.server.config.ConfigEntry; import org.opends.server.config.ConfigException; import org.opends.server.config.DNConfigAttribute; import org.opends.server.config.StringConfigAttribute; import org.opends.server.core.BindOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PasswordPolicyState; import org.opends.server.protocols.asn1.ASN1OctetString; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.types.AuthenticationInfo; import org.opends.server.types.ByteString; import org.opends.server.types.ConfigChangeResult; import org.opends.server.types.DirectoryException; import org.opends.server.types.DisconnectReason; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.ErrorLogCategory; import org.opends.server.types.ErrorLogSeverity; import org.opends.server.types.InitializationException; import org.opends.server.types.LockManager; import org.opends.server.types.Privilege; import org.opends.server.types.ResultCode; import org.opends.server.util.Base64; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.loggers.debug.DebugLogger.debugCaught; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import org.opends.server.types.DebugLogLevel; import static org.opends.server.loggers.Error.*; import static org.opends.server.messages.ExtensionsMessages.*; import static org.opends.server.messages.MessageHandler.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class provides an implementation of a SASL mechanism that uses digest * authentication via DIGEST-MD5. This is a password-based mechanism that does * not expose the password itself over the wire but rather uses an MD5 hash that * proves the client knows the password. This is similar to the CRAM-MD5 * mechanism, and the primary differences are that CRAM-MD5 only obtains random * data from the server whereas DIGEST-MD5 uses random data from both the * server and the client, CRAM-MD5 does not allow for an authorization ID in * addition to the authentication ID where DIGEST-MD5 does, and CRAM-MD5 does * not define any integrity and confidentiality mechanisms where DIGEST-MD5 * does. This implementation is based on the specification in RFC 2831 and * updates from draft-ietf-sasl-rfc2831bis-06. */ public class DigestMD5SASLMechanismHandler extends SASLMechanismHandler implements ConfigurableComponent { // The DN of the configuration entry for this SASL mechanism handler. private DN configEntryDN; // The DN of the identity mapper configuration entry. private DN identityMapperDN; // The identity mapper that will be used to map ID strings to user entries. private IdentityMapper identityMapper; // The message digest engine that will be used to create the MD5 digests. private MessageDigest md5Digest; // The lock that will be used to provide threadsafe access to the message // digest. private ReentrantLock digestLock; // The random number generator that we will use to create the nonce. private SecureRandom randomGenerator; // The realm that the server should use, if one has been specified. private String realm; /** * 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 DigestMD5SASLMechanismHandler() { super(); } /** * {@inheritDoc} */ @Override() public void initializeSASLMechanismHandler(ConfigEntry configEntry) throws ConfigException, InitializationException { this.configEntryDN = configEntry.getDN(); // Initialize the variables needed for the MD5 digest creation. digestLock = new ReentrantLock(); randomGenerator = new SecureRandom(); try { md5Digest = MessageDigest.getInstance("MD5"); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } int msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_MESSAGE_DIGEST; String message = getMessage(msgID, stackTraceToSingleLineString(e)); throw new InitializationException(msgID, message, e); } // Get the identity mapper that should be used to find users. int msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_IDENTITY_MAPPER_DN; DNConfigAttribute mapperStub = new DNConfigAttribute(ATTR_IDMAPPER_DN, getMessage(msgID), true, false, false); try { DNConfigAttribute mapperAttr = (DNConfigAttribute) configEntry.getConfigAttribute(mapperStub); if (mapperAttr == null) { msgID = MSGID_SASLDIGESTMD5_NO_IDENTITY_MAPPER_ATTR; String message = getMessage(msgID, String.valueOf(configEntryDN)); throw new ConfigException(msgID, message); } else { identityMapperDN = mapperAttr.activeValue(); identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); if (identityMapper == null) { msgID = MSGID_SASLDIGESTMD5_NO_SUCH_IDENTITY_MAPPER; String message = getMessage(msgID, String.valueOf(identityMapperDN), String.valueOf(configEntryDN)); throw new ConfigException(msgID, message); } } } catch (ConfigException ce) { throw ce; } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_IDENTITY_MAPPER; String message = getMessage(msgID, String.valueOf(configEntryDN), stackTraceToSingleLineString(e)); throw new InitializationException(msgID, message, e); } // Determine the realm to use, if any. realm = null; msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_REALM; StringConfigAttribute realmStub = new StringConfigAttribute(ATTR_DIGESTMD5_REALM, getMessage(msgID), false, false, false); try { StringConfigAttribute realmAttr = (StringConfigAttribute) configEntry.getConfigAttribute(realmStub); if (realmAttr != null) { realm = realmAttr.activeValue(); } } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_REALM; String message = getMessage(msgID, String.valueOf(configEntryDN), stackTraceToSingleLineString(e)); throw new InitializationException(msgID, message, e); } DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_DIGEST_MD5, this); DirectoryServer.registerConfigurableComponent(this); } /** * {@inheritDoc} */ @Override() public void finalizeSASLMechanismHandler() { DirectoryServer.deregisterConfigurableComponent(this); DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_DIGEST_MD5); } /** * {@inheritDoc} */ @Override() public void processSASLBind(BindOperation bindOperation) { // The DIGEST-MD5 bind process uses two stages. See if the client provided // any credentials. If not, then this is an initial authentication so we // will send a challenge to the client. ByteString clientCredentials = bindOperation.getSASLCredentials(); ClientConnection clientConnection = bindOperation.getClientConnection(); if ((clientCredentials == null) || (clientCredentials.value().length == 0)) { // Create a buffer to hold the challenge. StringBuilder challengeBuffer = new StringBuilder(); // Add the realm to the challenge. If we have a configured realm, then // use it. Otherwise, add a realm for each suffix defined in the server. if (realm == null) { Map suffixes = DirectoryServer.getPublicNamingContexts(); if (! suffixes.isEmpty()) { Iterator iterator = suffixes.keySet().iterator(); challengeBuffer.append("realm=\""); challengeBuffer.append(iterator.next().toNormalizedString()); challengeBuffer.append("\""); while (iterator.hasNext()) { challengeBuffer.append(",realm=\""); challengeBuffer.append(iterator.next().toNormalizedString()); challengeBuffer.append("\""); } } } else { challengeBuffer.append("realm=\""); challengeBuffer.append(realm); challengeBuffer.append("\""); } // Generate the nonce. Add it to the challenge and remember it for future // use. String nonce = generateNonce(); if (challengeBuffer.length() > 0) { challengeBuffer.append(","); } challengeBuffer.append("nonce=\""); challengeBuffer.append(nonce); challengeBuffer.append("\""); // Generate the qop-list and add it to the challenge. // FIXME -- Add support for integrity and confidentiality. Once we do, // we'll also want to add the maxbuf and cipher options. challengeBuffer.append(",qop=\"auth\""); // Add the charset option to indicate that we support UTF-8 values. challengeBuffer.append(",charset=utf-8"); // Add the algorithm, which will always be "md5-sess". challengeBuffer.append(",algorithm=md5-sess"); // Encode the challenge as an ASN.1 element. The total length of the // encoded value must be less than 2048 bytes, which should not be a // problem, but we'll add a safety check just in case.... In the event // that it does happen, we'll also log an error so it is more noticeable. ASN1OctetString challenge = new ASN1OctetString(challengeBuffer.toString()); if (challenge.value().length >= 2048) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CHALLENGE_TOO_LONG; String message = getMessage(msgID, challenge.value().length); bindOperation.setAuthFailureReason(msgID, message); logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_WARNING, message, msgID); return; } // Store the state information with the client connection so we can use it // for later validation. DigestMD5StateInfo stateInfo = new DigestMD5StateInfo(nonce, "00000000"); clientConnection.setSASLAuthStateInfo(stateInfo); // Prepare the response and return so it will be sent to the client. bindOperation.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS); bindOperation.setServerSASLCredentials(challenge); return; } // If we've gotten here, then the client did provide credentials. This can // be either an initial or subsequent authentication, but they will both be // handled identically. First, get the stored client SASL state. If it's // not there, then fail. Object saslStateInfo = clientConnection.getSASLAuthStateInfo(); if (saslStateInfo == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_STORED_STATE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } if (! (saslStateInfo instanceof DigestMD5StateInfo)) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INVALID_STORED_STATE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } DigestMD5StateInfo stateInfo = (DigestMD5StateInfo) saslStateInfo; // Create variables to hold values stored in the client's response. We'll // also store the base DN because we might need to override it later. String responseUserName = null; String responseRealm = null; String responseNonce = null; String responseCNonce = null; int responseNonceCount = -1; String responseNonceCountStr = null; String responseQoP = "auth"; String responseDigestURI = null; byte[] responseDigest = null; String responseCharset = "ISO-8859-1"; String responseAuthzID = null; // Get a temporary string representation of the SASL credentials using the // ISO-8859-1 encoding and see if it contains "charset=utf-8". If so, then // re-parse the credentials using that character set. byte[] credBytes = clientCredentials.value(); String credString = null; String lowerCreds = null; try { credString = new String(credBytes, responseCharset); lowerCreds = toLowerCase(credString); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } // This isn't necessarily fatal because we're going to retry using UTF-8, // but we want to log it anyway. logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_WARNING, MSGID_SASLDIGESTMD5_CANNOT_PARSE_ISO_CREDENTIALS, responseCharset, stackTraceToSingleLineString(e)); } if ((credString == null) || (lowerCreds.indexOf("charset=utf-8") >= 0)) { try { credString = new String(credBytes, "UTF-8"); lowerCreds = toLowerCase(credString); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } // This is fatal because either we can't parse the credentials as a // string at all, or we know we need to do so using UTF-8 and can't. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_PARSE_UTF8_CREDENTIALS; String message = getMessage(msgID, stackTraceToSingleLineString(e)); bindOperation.setAuthFailureReason(msgID, message); return; } } // Iterate through the credentials string, parsing the property names and // their corresponding values. int pos = 0; int length = credString.length(); while (pos < length) { int equalPos = credString.indexOf('=', pos+1); if (equalPos < 0) { // This is bad because we're not at the end of the string but we don't // have a name/value delimiter. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS; String message = getMessage(msgID, pos); bindOperation.setAuthFailureReason(msgID, message); return; } String tokenName = lowerCreds.substring(pos, equalPos); String tokenValue; try { StringBuilder valueBuffer = new StringBuilder(); pos = readToken(credString, equalPos+1, length, valueBuffer); tokenValue = valueBuffer.toString(); } catch (DirectoryException de) { // We couldn't parse the token value, so it must be malformed. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); bindOperation.setAuthFailureReason(de.getErrorMessageID(), de.getErrorMessage()); return; } if (tokenName.equals("charset")) { // The value must be the string "utf-8". If not, that's an error. if (! tokenValue.equalsIgnoreCase("utf-8")) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INVALID_CHARSET; String message = getMessage(msgID, tokenValue); bindOperation.setAuthFailureReason(msgID, message); return; } } else if (tokenName.equals("username")) { responseUserName = tokenValue; } else if (tokenName.equals("realm")) { responseRealm = tokenValue; if (realm != null) { if (! responseRealm.equals(realm)) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INVALID_REALM; String message = getMessage(msgID, responseRealm); bindOperation.setAuthFailureReason(msgID, message); return; } } } else if (tokenName.equals("nonce")) { responseNonce = tokenValue; String requestNonce = stateInfo.getNonce(); if (! responseNonce.equals(requestNonce)) { // The nonce provided by the client is incorrect. This could be an // attempt at a replay or chosen plaintext attack, so we'll close the // connection. We will put a message in the log but will not send it // to the client. int msgID = MSGID_SASLDIGESTMD5_INVALID_NONCE; String message = getMessage(msgID); clientConnection.disconnect(DisconnectReason.SECURITY_PROBLEM, false, msgID, message); return; } } else if (tokenName.equals("cnonce")) { responseCNonce = tokenValue; } else if (tokenName.equals("nc")) { try { responseNonceCountStr = tokenValue; responseNonceCount = Integer.parseInt(responseNonceCountStr, 16); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_DECODE_NONCE_COUNT; String message = getMessage(msgID, tokenValue); bindOperation.setAuthFailureReason(msgID, message); return; } int storedNonce; try { storedNonce = Integer.parseInt(stateInfo.getNonceCount(), 16); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_DECODE_STORED_NONCE_COUNT; String message = getMessage(msgID, stackTraceToSingleLineString(e)); bindOperation.setAuthFailureReason(msgID, message); return; } if (responseNonceCount != (storedNonce + 1)) { // The nonce count provided by the client is incorrect. This // indicates a replay attack, so we'll close the connection. We will // put a message in the log but we will not send it to the client. int msgID = MSGID_SASLDIGESTMD5_INVALID_NONCE_COUNT; String message = getMessage(msgID); clientConnection.disconnect(DisconnectReason.SECURITY_PROBLEM, false, msgID, message); return; } } else if (tokenName.equals("qop")) { responseQoP = tokenValue; if (responseQoP.equals("auth")) { // No action necessary. } else if (responseQoP.equals("auth-int")) { // FIXME -- Add support for integrity protection. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INTEGRITY_NOT_SUPPORTED; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else if (responseQoP.equals("auth-conf")) { // FIXME -- Add support for confidentiality protection. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CONFIDENTIALITY_NOT_SUPPORTED; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else { // This is an invalid QoP value. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INVALID_QOP; String message = getMessage(msgID, responseQoP); bindOperation.setAuthFailureReason(msgID, message); return; } } else if (tokenName.equals("digest-uri")) { responseDigestURI = tokenValue; // FIXME -- Add the ability to validate this URI, at least to check the // hostname. } else if (tokenName.equals("response")) { try { responseDigest = hexStringToByteArray(tokenValue); } catch (ParseException pe) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, pe); } int msgID = MSGID_SASLDIGESTMD5_CANNOT_PARSE_RESPONSE_DIGEST; String message = getMessage(msgID, stackTraceToSingleLineString(pe)); bindOperation.setAuthFailureReason(msgID, message); return; } } else if (tokenName.equals("authzid")) { responseAuthzID = tokenValue; // FIXME -- This must always be parsed in UTF-8 even if the charset for // other elements is ISO 8859-1. } else if (tokenName.equals("maxbuf") || tokenName.equals("cipher")) { // FIXME -- Add support for confidentiality and integrity protection. } else { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INVALID_RESPONSE_TOKEN; String message = getMessage(msgID, tokenName); bindOperation.setAuthFailureReason(msgID, message); return; } } // Make sure that all required properties have been specified. if ((responseUserName == null) || (responseUserName.length() == 0)) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_USERNAME_IN_RESPONSE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else if (responseNonce == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_NONCE_IN_RESPONSE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else if (responseCNonce == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_CNONCE_IN_RESPONSE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else if (responseNonceCount < 0) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_NONCE_COUNT_IN_RESPONSE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else if (responseDigestURI == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_DIGEST_URI_IN_RESPONSE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else if (responseDigest == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_DIGEST_IN_RESPONSE; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } // If a realm has not been specified, then use the empty string. // FIXME -- Should we reject this if a specific realm is defined? if (responseRealm == null) { responseRealm = ""; } // Get the user entry for the authentication ID. Allow for an // authentication ID that is just a username (as per the DIGEST-MD5 spec), // but also allow a value in the authzid form specified in RFC 2829. Entry userEntry = null; String lowerUserName = toLowerCase(responseUserName); if (lowerUserName.startsWith("dn:")) { // Try to decode the user DN and retrieve the corresponding entry. DN userDN; try { userDN = DN.decode(responseUserName.substring(3)); } catch (DirectoryException de) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_DECODE_USERNAME_AS_DN; String message = getMessage(msgID, responseUserName, de.getErrorMessage()); bindOperation.setAuthFailureReason(msgID, message); return; } if (userDN.isNullDN()) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_USERNAME_IS_NULL_DN; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, 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. Lock readLock = null; for (int i=0; i < 3; i++) { readLock = LockManager.lockRead(userDN); if (readLock != null) { break; } } if (readLock == null) { bindOperation.setResultCode(DirectoryServer.getServerErrorResultCode()); int msgID = MSGID_SASLDIGESTMD5_CANNOT_LOCK_ENTRY; String message = getMessage(msgID, String.valueOf(userDN)); bindOperation.setAuthFailureReason(msgID, message); return; } try { userEntry = DirectoryServer.getEntry(userDN); } catch (DirectoryException de) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_ENTRY_BY_DN; String message = getMessage(msgID, String.valueOf(userDN), de.getErrorMessage()); bindOperation.setAuthFailureReason(msgID, message); return; } finally { LockManager.unlock(userDN, readLock); } } else { // Use the identity mapper to resolve the username to an entry. String userName = responseUserName; if (lowerUserName.startsWith("u:")) { if (lowerUserName.equals("u:")) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_ZERO_LENGTH_USERNAME; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } userName = responseUserName.substring(2); } try { userEntry = identityMapper.getEntryForID(userName); } catch (DirectoryException de) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_MAP_USERNAME; String message = getMessage(msgID, String.valueOf(responseUserName), de.getErrorMessage()); bindOperation.setAuthFailureReason(msgID, 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); int msgID = MSGID_SASLDIGESTMD5_NO_MATCHING_ENTRIES; String message = getMessage(msgID, responseUserName); bindOperation.setAuthFailureReason(msgID, message); return; } else { bindOperation.setSASLAuthUserEntry(userEntry); } Entry authZEntry = userEntry; if (responseAuthzID != null) { if (responseAuthzID.length() == 0) { // The authorization ID must not be an empty string. bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_EMPTY_AUTHZID; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } else if (! responseAuthzID.equals(responseUserName)) { String lowerAuthzID = toLowerCase(responseAuthzID); if (lowerAuthzID.startsWith("dn:")) { DN authzDN; try { authzDN = DN.decode(responseAuthzID.substring(3)); } catch (DirectoryException de) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_AUTHZID_INVALID_DN; String message = getMessage(msgID, responseAuthzID, de.getErrorMessage()); bindOperation.setAuthFailureReason(msgID, 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); int msgID = MSGID_SASLDIGESTMD5_AUTHZID_INSUFFICIENT_PRIVILEGES; String message = getMessage(msgID, String.valueOf(userEntry.getDN())); bindOperation.setAuthFailureReason(msgID, message); return; } if (authzDN.isNullDN()) { authZEntry = null; } else { try { authZEntry = DirectoryServer.getEntry(authzDN); if (authZEntry == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_AUTHZID_NO_SUCH_ENTRY; String message = getMessage(msgID, String.valueOf(authzDN)); bindOperation.setAuthFailureReason(msgID, message); return; } } catch (DirectoryException de) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_AUTHZID_CANNOT_GET_ENTRY; String message = getMessage(msgID, String.valueOf(authzDN), de.getErrorMessage()); bindOperation.setAuthFailureReason(msgID, message); return; } } } } else { String idStr; if (lowerAuthzID.startsWith("u:")) { idStr = responseAuthzID.substring(2); } else { idStr = responseAuthzID; } if (idStr.length() == 0) { authZEntry = null; } else { try { authZEntry = identityMapper.getEntryForID(idStr); if (authZEntry == null) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_AUTHZID_NO_MAPPED_ENTRY; String message = getMessage(msgID, responseAuthzID); bindOperation.setAuthFailureReason(msgID, message); return; } } catch (DirectoryException de) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, de); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_MAP_AUTHZID; String message = getMessage(msgID, responseAuthzID, de.getErrorMessage()); bindOperation.setAuthFailureReason(msgID, 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); int msgID = MSGID_SASLDIGESTMD5_AUTHZID_INSUFFICIENT_PRIVILEGES; String message = getMessage(msgID, String.valueOf(userEntry.getDN())); bindOperation.setAuthFailureReason(msgID, message); return; } } } } } // Get the clear-text passwords from the user entry, if there are any. List clearPasswords; try { PasswordPolicyState pwPolicyState = new PasswordPolicyState(userEntry, false, false); clearPasswords = pwPolicyState.getClearPasswords(); if ((clearPasswords == null) || clearPasswords.isEmpty()) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_NO_REVERSIBLE_PASSWORDS; String message = getMessage(msgID, String.valueOf(userEntry.getDN())); bindOperation.setAuthFailureReason(msgID, message); return; } } catch (Exception e) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_REVERSIBLE_PASSWORDS; String message = getMessage(msgID, String.valueOf(userEntry.getDN()), String.valueOf(e)); bindOperation.setAuthFailureReason(msgID, message); return; } // Iterate through the clear-text values and see if any of them can be used // in conjunction with the challenge to construct the provided digest. boolean matchFound = false; byte[] passwordBytes = null; for (ByteString clearPassword : clearPasswords) { byte[] generatedDigest; try { generatedDigest = generateResponseDigest(responseUserName, responseAuthzID, clearPassword.value(), responseRealm, responseNonce, responseCNonce, responseNonceCountStr, responseDigestURI, responseQoP, responseCharset); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } logError(ErrorLogCategory.EXTENSIONS, ErrorLogSeverity.SEVERE_WARNING, MSGID_SASLDIGESTMD5_CANNOT_GENERATE_RESPONSE_DIGEST, stackTraceToSingleLineString(e)); continue; } if (Arrays.equals(responseDigest, generatedDigest)) { matchFound = true; passwordBytes = clearPassword.value(); break; } } if (! matchFound) { bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_INVALID_CREDENTIALS; String message = getMessage(msgID); bindOperation.setAuthFailureReason(msgID, message); return; } // Generate the response auth element to include in the response to the // client. byte[] responseAuth; try { responseAuth = generateResponseAuthDigest(responseUserName, responseAuthzID, passwordBytes, responseRealm, responseNonce, responseCNonce, responseNonceCountStr, responseDigestURI, responseQoP, responseCharset); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); int msgID = MSGID_SASLDIGESTMD5_CANNOT_GENERATE_RESPONSE_AUTH_DIGEST; String message = getMessage(msgID, stackTraceToSingleLineString(e)); bindOperation.setAuthFailureReason(msgID, message); return; } ASN1OctetString responseAuthStr = new ASN1OctetString("rspauth=" + getHexString(responseAuth)); // Make sure to store the updated nonce count with the client connection to // allow for correct subsequent authentication. stateInfo.setNonceCount(responseNonceCountStr); // If we've gotten here, then the authentication was successful. We'll also // need to include the response auth string in the server SASL credentials. bindOperation.setResultCode(ResultCode.SUCCESS); bindOperation.setServerSASLCredentials(responseAuthStr); AuthenticationInfo authInfo = new AuthenticationInfo(userEntry, authZEntry, SASL_MECHANISM_DIGEST_MD5, DirectoryServer.isRootDN(userEntry.getDN())); bindOperation.setAuthenticationInfo(authInfo); return; } /** * Generates a new nonce value to use during the DIGEST-MD5 authentication * process. * * @return The nonce that should be used for DIGEST-MD5 authentication. */ private String generateNonce() { byte[] nonceBytes = new byte[16]; digestLock.lock(); try { randomGenerator.nextBytes(nonceBytes); } finally { digestLock.unlock(); } return Base64.encode(nonceBytes); } /** * Reads the next token from the provided credentials string using the * provided information. If the token is surrounded by quotation marks, then * the token returned will not include those quotation marks. * * @param credentials The credentials string from which to read the token. * @param startPos The position of the first character of the token to * read. * @param length The total number of characters in the credentials * string. * @param token The buffer into which the token is to be placed. * * @return The position at which the next token should start, or a value * greater than or equal to the length of the string if there are no * more tokens. * * @throws DirectoryException If a problem occurs while attempting to read * the token. */ private int readToken(String credentials, int startPos, int length, StringBuilder token) throws DirectoryException { // If the position is greater than or equal to the length, then we shouldn't // do anything. if (startPos >= length) { return startPos; } // Look at the first character to see if it's an empty string or the string // is quoted. boolean isEscaped = false; boolean isQuoted = false; int pos = startPos; char c = credentials.charAt(pos++); if (c == ',') { // This must be a zero-length token, so we'll just return the next // position. return pos; } else if (c == '"') { // The string is quoted, so we'll ignore this character, and we'll keep // reading until we find the unescaped closing quote followed by a comma // or the end of the string. isQuoted = true; } else if (c == '\\') { // The next character is escaped, so we'll take it no matter what. isEscaped = true; } else { // The string is not quoted, and this is the first character. Store this // character and keep reading until we find a comma or the end of the // string. token.append(c); } // Enter a loop, reading until we find the appropriate criteria for the end // of the token. while (pos < length) { c = credentials.charAt(pos++); if (isEscaped) { // The previous character was an escape, so we'll take this no matter // what. token.append(c); isEscaped = false; } else if (c == ',') { // If this is a quoted string, then this comma is part of the token. // Otherwise, it's the end of the token. if (isQuoted) { token.append(c); } else { break; } } else if (c == '"') { if (isQuoted) { // This should be the end of the token, but in order for it to be // valid it must be followed by a comma or the end of the string. if (pos >= length) { // We have hit the end of the string, so this is fine. break; } else { char c2 = credentials.charAt(pos++); if (c2 == ',') { // We have hit the end of the token, so this is fine. break; } else { // We found the closing quote before the end of the token. This // is not fine. int msgID = MSGID_SASLDIGESTMD5_INVALID_CLOSING_QUOTE_POS; String message = getMessage(msgID, (pos-2)); throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message, msgID); } } } else { // This must be part of the value, so we'll take it. token.append(c); } } else if (c == '\\') { // The next character is escaped. We'll set a flag so we know to // accept it, but will not include the backspace itself. isEscaped = true; } else { token.append(c); } } return pos; } /** * Generates the appropriate DIGEST-MD5 response for the provided set of * information. * * @param userName The username from the authentication request. * @param authzID The authorization ID from the request, or * null if there is none. * @param password The clear-text password for the user. * @param realm The realm for which the authentication is to be * performed. * @param nonce The random data generated by the server for use in the * digest. * @param cnonce The random data generated by the client for use in the * digest. * @param nonceCount The 8-digit hex string indicating the number of times * the provided nonce has been used by the client. * @param digestURI The digest URI that specifies the service and host for * which the authentication is being performed. * @param qop The quality of protection string for the * authentication. * @param charset The character set used to encode the information. * * @return The DIGEST-MD5 response for the provided set of information. * * @throws UnsupportedEncodingException If the specified character set is * invalid for some reason. */ public byte[] generateResponseDigest(String userName, String authzID, byte[] password, String realm, String nonce, String cnonce, String nonceCount, String digestURI, String qop, String charset) throws UnsupportedEncodingException { digestLock.lock(); try { // First, get a hash of "username:realm:password". StringBuilder a1String1 = new StringBuilder(); a1String1.append(userName); a1String1.append(':'); a1String1.append(realm); a1String1.append(':'); byte[] a1Bytes1a = a1String1.toString().getBytes(charset); byte[] a1Bytes1 = new byte[a1Bytes1a.length + password.length]; System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length); System.arraycopy(password, 0, a1Bytes1, a1Bytes1a.length, password.length); byte[] urpHash = md5Digest.digest(a1Bytes1); // Next, get a hash of "urpHash:nonce:cnonce[:authzid]". StringBuilder a1String2 = new StringBuilder(); a1String2.append(':'); a1String2.append(nonce); a1String2.append(':'); a1String2.append(cnonce); if (authzID != null) { a1String2.append(':'); a1String2.append(authzID); } byte[] a1Bytes2a = a1String2.toString().getBytes(charset); byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length]; System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length); System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length); byte[] a1Hash = md5Digest.digest(a1Bytes2); // Next, get a hash of "AUTHENTICATE:digesturi". byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset); byte[] a2Hash = md5Digest.digest(a2Bytes); // Get hex string representations of the last two hashes. String a1HashHex = getHexString(a1Hash); String a2HashHex = getHexString(a2Hash); // Put together the final string to hash, consisting of // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest. StringBuilder kdString = new StringBuilder(); kdString.append(a1HashHex); kdString.append(':'); kdString.append(nonce); kdString.append(':'); kdString.append(nonceCount); kdString.append(':'); kdString.append(cnonce); kdString.append(':'); kdString.append(qop); kdString.append(':'); kdString.append(a2HashHex); return md5Digest.digest(kdString.toString().getBytes(charset)); } finally { digestLock.unlock(); } } /** * Generates the appropriate DIGEST-MD5 rspauth digest using the provided * information. * * @param userName The username from the authentication request. * @param authzID The authorization ID from the request, or * null if there is none. * @param password The clear-text password for the user. * @param realm The realm for which the authentication is to be * performed. * @param nonce The random data generated by the server for use in the * digest. * @param cnonce The random data generated by the client for use in the * digest. * @param nonceCount The 8-digit hex string indicating the number of times * the provided nonce has been used by the client. * @param digestURI The digest URI that specifies the service and host for * which the authentication is being performed. * @param qop The quality of protection string for the * authentication. * @param charset The character set used to encode the information. * * @return The DIGEST-MD5 response for the provided set of information. * * @throws UnsupportedEncodingException If the specified character set is * invalid for some reason. */ public byte[] generateResponseAuthDigest(String userName, String authzID, byte[] password, String realm, String nonce, String cnonce, String nonceCount, String digestURI, String qop, String charset) throws UnsupportedEncodingException { digestLock.lock(); try { // First, get a hash of "username:realm:password". StringBuilder a1String1 = new StringBuilder(); a1String1.append(userName); a1String1.append(':'); a1String1.append(realm); a1String1.append(':'); byte[] a1Bytes1a = a1String1.toString().getBytes(charset); byte[] a1Bytes1 = new byte[a1Bytes1a.length + password.length]; System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length); System.arraycopy(password, 0, a1Bytes1, a1Bytes1a.length, password.length); byte[] urpHash = md5Digest.digest(a1Bytes1); // Next, get a hash of "urpHash:nonce:cnonce[:authzid]". StringBuilder a1String2 = new StringBuilder(); a1String2.append(':'); a1String2.append(nonce); a1String2.append(':'); a1String2.append(cnonce); if (authzID != null) { a1String2.append(':'); a1String2.append(authzID); } byte[] a1Bytes2a = a1String2.toString().getBytes(charset); byte[] a1Bytes2 = new byte[urpHash.length + a1Bytes2a.length]; System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length); System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length); byte[] a1Hash = md5Digest.digest(a1Bytes2); // Next, get a hash of "AUTHENTICATE:digesturi". String a2String = ":" + digestURI; if (qop.equals("auth-int") || qop.equals("auth-conf")) { a2String += ":00000000000000000000000000000000"; } byte[] a2Bytes = a2String.getBytes(charset); byte[] a2Hash = md5Digest.digest(a2Bytes); // Get hex string representations of the last two hashes. String a1HashHex = getHexString(a1Hash); String a2HashHex = getHexString(a2Hash); // Put together the final string to hash, consisting of // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest. StringBuilder kdString = new StringBuilder(); kdString.append(a1HashHex); kdString.append(':'); kdString.append(nonce); kdString.append(':'); kdString.append(nonceCount); kdString.append(':'); kdString.append(cnonce); kdString.append(':'); kdString.append(qop); kdString.append(':'); kdString.append(a2HashHex); return md5Digest.digest(kdString.toString().getBytes(charset)); } finally { digestLock.unlock(); } } /** * Retrieves a hexadecimal string representation of the contents of the * provided byte array. * * @param byteArray The byte array for which to obtain the hexadecimal * string representation. * * @return The hexadecimal string representation of the contents of the * provided byte array. */ private String getHexString(byte[] byteArray) { StringBuilder buffer = new StringBuilder(2*byteArray.length); for (byte b : byteArray) { buffer.append(byteToLowerHex(b)); } return buffer.toString(); } /** * Retrieves the DN of the configuration entry with which this component is * associated. * * @return The DN of the configuration entry with which this component is * associated. */ public DN getConfigurableComponentEntryDN() { return configEntryDN; } /** * Retrieves the set of configuration attributes that are associated with this * configurable component. * * @return The set of configuration attributes that are associated with this * configurable component. */ public List getConfigurationAttributes() { LinkedList attrList = new LinkedList(); int msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_IDENTITY_MAPPER_DN; attrList.add(new DNConfigAttribute(ATTR_IDMAPPER_DN, getMessage(msgID), true, false, false, identityMapperDN)); msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_REALM; attrList.add(new StringConfigAttribute(ATTR_DIGESTMD5_REALM, getMessage(msgID), false, false, false, realm)); return attrList; } /** * Indicates whether the provided configuration entry has an acceptable * configuration for this component. If it does not, then detailed * information about the problem(s) should be added to the provided list. * * @param configEntry The configuration entry for which to make the * determination. * @param unacceptableReasons A list that can be used to hold messages about * why the provided entry does not have an * acceptable configuration. * * @return true if the provided entry has an acceptable * configuration for this component, or false if not. */ public boolean hasAcceptableConfiguration(ConfigEntry configEntry, List unacceptableReasons) { // Look at the identity mapper configuration. int msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_IDENTITY_MAPPER_DN; DNConfigAttribute mapperStub = new DNConfigAttribute(ATTR_IDMAPPER_DN, getMessage(msgID), true, false, false); try { DNConfigAttribute mapperAttr = (DNConfigAttribute) configEntry.getConfigAttribute(mapperStub); if (mapperAttr == null) { msgID = MSGID_SASLDIGESTMD5_NO_IDENTITY_MAPPER_ATTR; unacceptableReasons.add(getMessage(msgID, String.valueOf(configEntryDN))); return false; } DN mapperDN = mapperAttr.pendingValue(); if (! mapperDN.equals(identityMapperDN)) { IdentityMapper mapper = DirectoryServer.getIdentityMapper(mapperDN); if (mapper == null) { msgID = MSGID_SASLDIGESTMD5_NO_SUCH_IDENTITY_MAPPER; unacceptableReasons.add(getMessage(msgID, String.valueOf(mapperDN), String.valueOf(configEntryDN))); return false; } } } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_IDENTITY_MAPPER; unacceptableReasons.add(getMessage(msgID, String.valueOf(configEntryDN), stackTraceToSingleLineString(e))); return false; } // Look at the realm configuration. msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_REALM; StringConfigAttribute realmStub = new StringConfigAttribute(ATTR_DIGESTMD5_REALM, getMessage(msgID), false, false, false); try { StringConfigAttribute realmAttr = (StringConfigAttribute) configEntry.getConfigAttribute(realmStub); } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_REALM; unacceptableReasons.add(getMessage(msgID, String.valueOf(configEntryDN), stackTraceToSingleLineString(e))); return false; } // If we've gotten to this point, then everything must be OK. return true; } /** * Makes a best-effort attempt to apply the configuration contained in the * provided entry. Information about the result of this processing should be * added to the provided message list. Information should always be added to * this list if a configuration change could not be applied. If detailed * results are requested, then information about the changes applied * successfully (and optionally about parameters that were not changed) should * also be included. * * @param configEntry The entry containing the new configuration to * apply for this component. * @param detailedResults Indicates whether detailed information about the * processing should be added to the list. * * @return Information about the result of the configuration update. */ public ConfigChangeResult applyNewConfiguration(ConfigEntry configEntry, boolean detailedResults) { ResultCode resultCode = ResultCode.SUCCESS; boolean adminActionRequired = false; ArrayList messages = new ArrayList(); // Look at the identity mapper configuration. DN newIdentityMapperDN = null; IdentityMapper newIdentityMapper = null; int msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_IDENTITY_MAPPER_DN; DNConfigAttribute mapperStub = new DNConfigAttribute(ATTR_IDMAPPER_DN, getMessage(msgID), true, false, false); try { DNConfigAttribute mapperAttr = (DNConfigAttribute) configEntry.getConfigAttribute(mapperStub); if (mapperAttr == null) { msgID = MSGID_SASLDIGESTMD5_NO_IDENTITY_MAPPER_ATTR; messages.add(getMessage(msgID, String.valueOf(configEntryDN))); resultCode = ResultCode.CONSTRAINT_VIOLATION; } else { newIdentityMapperDN = mapperAttr.pendingValue(); if (! newIdentityMapperDN.equals(identityMapperDN)) { newIdentityMapper = DirectoryServer.getIdentityMapper(newIdentityMapperDN); if (newIdentityMapper == null) { msgID = MSGID_SASLDIGESTMD5_NO_SUCH_IDENTITY_MAPPER; messages.add(getMessage(msgID, String.valueOf(newIdentityMapperDN), String.valueOf(configEntryDN))); resultCode = ResultCode.CONSTRAINT_VIOLATION; } } } } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_IDENTITY_MAPPER; messages.add(getMessage(msgID, String.valueOf(configEntryDN), stackTraceToSingleLineString(e))); resultCode = DirectoryServer.getServerErrorResultCode(); } // Look at the realm configuration. String newRealm = null; msgID = MSGID_SASLDIGESTMD5_DESCRIPTION_REALM; StringConfigAttribute realmStub = new StringConfigAttribute(ATTR_DIGESTMD5_REALM, getMessage(msgID), false, false, false); try { StringConfigAttribute realmAttr = (StringConfigAttribute) configEntry.getConfigAttribute(realmStub); if (realmAttr != null) { newRealm = realmAttr.pendingValue(); } } catch (Exception e) { if (debugEnabled()) { debugCaught(DebugLogLevel.ERROR, e); } msgID = MSGID_SASLDIGESTMD5_CANNOT_GET_REALM; messages.add(getMessage(msgID, String.valueOf(configEntryDN), stackTraceToSingleLineString(e))); if (resultCode == ResultCode.SUCCESS) { resultCode = DirectoryServer.getServerErrorResultCode(); } } // If everything has been successful, then apply any changes that were made. if (resultCode == ResultCode.SUCCESS) { if ((newIdentityMapperDN != null) && (newIdentityMapper != null)) { identityMapperDN = newIdentityMapperDN; identityMapper = newIdentityMapper; if (detailedResults) { msgID = MSGID_SASLDIGESTMD5_UPDATED_IDENTITY_MAPPER; messages.add(getMessage(msgID, String.valueOf(configEntryDN), String.valueOf(identityMapperDN))); } } if (realm == null) { if (newRealm != null) { realm = newRealm; if (detailedResults) { msgID = MSGID_SASLDIGESTMD5_UPDATED_NEW_REALM; messages.add(getMessage(msgID, String.valueOf(configEntryDN), String.valueOf(realm))); } } } else if (newRealm == null) { realm = null; if (detailedResults) { msgID = MSGID_SASLDIGESTMD5_UPDATED_NO_REALM; messages.add(getMessage(msgID, String.valueOf(configEntryDN))); } } else { if (! realm.equals(newRealm)) { realm = newRealm; if (detailedResults) { msgID = MSGID_SASLDIGESTMD5_UPDATED_NEW_REALM; messages.add(getMessage(msgID, String.valueOf(configEntryDN), String.valueOf(realm))); } } } } // Return the result to the caller. return new ConfigChangeResult(resultCode, adminActionRequired, messages); } /** * {@inheritDoc} */ @Override() public boolean isPasswordBased(String mechanism) { // This is a password-based mechanism. return true; } /** * {@inheritDoc} */ @Override() public boolean isSecure(String mechanism) { // This may be considered a secure mechanism. return true; } }