Sync password updates from ForgeRock Identity Cloud to a remote LDAP directory

By Kevin Schneider
Originally posted on https://medium.com/@kevsuisse


This post discusses how password changes in ForgeRock Identity Cloud can be pushed to remote LDAP directories such as Active Directory or an on-prem ForgeRock Directory Services.

Let’s first discuss how passwords are stored and what implications this has when trying to sync them between different systems.

Hashing algorithms

In ForgeRock Identity Cloud, passwords are stored in a way that makes it practically impossible to retrieve the cleartext. It makes use of so-called one-way functions or hash functions that are specifically designed for password storage (list of supported hash algorithms). Apart from it being impossible to decrypt and get the cleartext, the algorithms are purposely designed to be slow in computing the hash value and thus make brute force attacks impractical. When the password is stored, it is immediately converted to a hash making it impossible for anyone (including ForgeRock) to retrieve the cleartext value.

Storing passwords in this way is extremely important in case the password database should ever be leaked. The only way to retrieve the cleartext is to try to hash every possible combination of characters and compare the resulting hashes. The use of salt and pepper (random bits added to each individual password to make them unique) means that this has to be done for every individual password separately and shortcuts like rainbow tables are useless.

What about encryption

Encryption is another way to securely store passwords. Typically this is done using symmetric encryption algorithms such as AES. These algorithms are also considered extremely secure and without access to the encryption key, it is practically impossible to get to the cleartext value. Yet herein lies the problem. The encryption keys have to be stored on the system and be accessible to it to encrypt the passwords. If an attacker manages to retrieve the password database, there is a reasonable chance that they also manage to retrieve the keys. It also opens up the possibility of an insider attack, where highly privileged administrators can access and misuse the keys.

It is therefore best practice to never store passwords in a recoverable way.

A practical problem

A common use case in ForgeRock Identity Cloud is to share passwords with other systems, so a user can log in using the same password. While ideally this is prevented by federating with a common identity provider so the password is only stored in ForgeRock Identity Cloud, this is not always possible or practical. If both ForgeRock Identity Cloud and the external system support the same algorithms, ForgeRock Identity Cloud can update the remote system with the already hashed password.

However, the unrecoverable nature of hashes can cause an issue if passwords are shared between different systems that don’t support the same algorithms.

Microsoft Active Directory for example stores passwords in a variety of proprietary hashes but not the PBKDF2 used in ForgeRock Identity Cloud. The only way to do it is to update the unicodePwd attribute in Active Directory via an LDAP connector and let Active Directory hash it with an algorithm it understands.

This gives us two options to solve the issue in ForgeRock Identity Cloud

  1. Store the password encrypted and use the sync engine
  2. Directly patch the external system in an onUpdate hook

Both methods have benefits and drawbacks. Storing the password as an encrypted value makes the sync simple and all the benefits of the sync engine can be used. However, as described above it greatly increases the risk of password leaks.

The remainder of the article will therefore concentrate on the second method.

Patching remote systems using an IDM onUpdate hook

When a user’s password changes, there will be a call to update the managed user object.

ForgeRock Identity Cloud lets us intercept that update in an onUpdate hook on the user managed object. This allows us to handle the password before it is stored and hashed.

The following assumes that there an LDAP connector has already been set up via a Remote Connector Server (RCS). Importantly, the connector MUST be configured to use LDAPS. This is required to ensure the LDAP call from the RCS to the directory server is encrypted. The connection between ForgeRock Identity Cloud and the ForgeRock Remote Connector Server (RCS) is always encrypted by the Secure Web Socket (WSS) protocol. The LDAP service user used by the connector MUST have permission to change other users’ passwords.

For Active Directory, the user must have delegated permission to “Reset user passwords and force password change at next logon” as well as “Read all user information”

Setup the onUpdate script

In your ForgeRock Identity Cloud tenant, head to Native Console > Identity Management and then Configuration >Managed Objects

Once there, select the alpha_user managed object. (You can do the same with bravo_user, just replace alpha_user with bravo_user in the following steps and in the script)

Then select the Scripts tab of the alpha_use

Add the following script as an onUpdate event script

(function () {
  /**
   * configuration
   */
  const config = {
    nodeName: "alpha_user-onUpdate",
    ldapUserNameAttribute: "sAMAccountName",
    ldapConnectorName: "LdapConnector",
  };

  /**
   * Helper function to tag a message for logging purposes
   * @param {} message the message to be tagged
   * @returns tagged message
   */
  function _tag(message) {
    return `${config.nodeName}:: ${message}`;
  }

  // Script entry point
  if (
    oldObject.password !== object.password &&
    object.password !== null &&
    object.password !== ""
  ) {

    const query = {
      _queryFilter: `${config.ldapUserNameAttribute} eq "${object.userName}"`,
    };

    const ldapUserQuery = openidm.query(
      `system/${config.ldapConnectorName}/account`,
      query
    );

    if (!!ldapUserQuery && ldapUserQuery.resultCount === 1) {
      try {
        const ldapUser = ldapUserQuery.result[0];
        const patch = [
          {
            operation: "replace",
            field: "/password",
            value: object.password
          },
        ];
        const systemUser = `system/${config.ldapConnectorName}/account/${ldapUser._id}`;

        openidm.patch(systemUser, null, patch);
        logger.debug(_tag("successfully updated password in remote system"));

      } catch (e) {
        logger.error(_tag("Exception while patching password: " + e));
        throw {
          code: 400,
          message: "Error while updating password in remote directory",
        };
      }
    } else {
      logger.error(_tag("Unable to find LDAP user " + object.userName));
      throw {
        code: 400,
        message: "Error while updating password. Unable to find LDAP user",
      };
    }
  }

  require("onUpdateUser").preserveLastSync(object, oldObject, request);
  
})();

Script Breakdown

Let’s break the script down into its components

const config = {
nodeName: "alpha_user-onUpdate",
ldapUserNameAttribute: "sAMAccountName",
ldapConnectorName: "LdapConnector"
};

The config variable sets some basic values. Change these to adapt to your configuration

  • nodeName is used as a prefix for log messages
  • ldapUserNameAttribute is the name of the username attribute in the remote directory. For Active Directory this is sAMAccountName and for ForgeRock Directory Services it is userName.
  • ldapConnectorName is the name of the LDAP connector to the remote directory.
if(oldObject.password !== object.password 
 && object.password !== null
&& object.password !== "") {

First, we check that we only attempt to change a password if it has changed, not been set to null or an empty value.

const query = {
      _queryFilter: `${config.ldapUserNameAttribute} eq "${object.userName}"`,
    };

    const ldapUserQuery = openidm.query(
      `system/${config.ldapConnectorName}/account`,
      query
    );

    if (!!ldapUserQuery && ldapUserQuery.resultCount === 1) {
      
[...]

      }
    } else {
      logger.error(_tag("Unable to find LDAP user " + object.userName));
      throw {
        code: 400,
        message: "Error while updating password. Unable to find LDAP user",
      };
    }

Then we do a lookup of the user in the remote directory. This is to confirm the user exists in the remote LDAP service and to get the _id of the user object. The _id depends on the connector configuration uidAttribute and can for example be the entryUUID of the object in the remote LDAP*.* We only proceed if we receive exactly one result. If the lookup fails, we throw an error.

 try {
        const ldapUser = ldapUserQuery.result[0];
        const patch = [
          {
            operation: "replace",
            field: "/password",
            value: object.password
          },
        ];
        const systemUser = `system/${config.ldapConnectorName}/account/${ldapUser._id}`;

        openidm.patch(systemUser, null, patch);

      } catch (e) {
        logger.error(_tag("Exception while patching password: " + e));
        throw {
          code: 400,
          message: "Error while updating password in remote directory",
        };
      }

The next block executes the PATCH operation. This is being translated into an LDAP modify command by the connector and sent to the remote directory.

If the operation fails, we throw an error and log the exception.

Conclusion

Updating the password in a remote LDAP service can be achieved with a simple script and without increasing the attack surface. It makes use of the secure connection provided by the ForgeRock Remote Connector Server and the flexibility that Managed Object event hooks provide.

Other Articles by This Author

1 Like