ForgeRock Identity Cloud: Password Expiration Warnings in AuthN Journeys

Originally posted on keithdaly-identity.medium.com

Method for Implementing Password Expiration Warnings

As a ForgeRock Identity Cloud customer, you will still perform administrative and development tasks on the platform related to functionality, but you will no longer be responsible for architecting, deploying, upgrading, managing, and monitoring the infrastructure and components. ForgeRock will ensure that performance and availability service levels are met.

When it comes to the directory component, ForgeRock will define and manage directory configuration, including data replication, security configuration, etc. Once restriction of this model is that direct LDAP access to the cloud-hosted Directory Services (DS) instance via LDAP(s) is not available to customers.

With our self-managed Directory Services (on-prem), customers can directly view the applicable password policies and view the technical attributes on user accounts that are used for password management. In ID Cloud, password policies can be managed in the platform UI, but these underlying policy attributes on the user account are not currently visible. Therefore, in ID Cloud it is not possible to directly query the existing operational attributes required to create a password expiration warning mechanism.

This post provides a solution for the password expiration warning use case.

Solution Step 1: Create/Claim/Reserve a “Password Expiration” Field

ForgeRock provides 5 instances of generic attributes for each attribute type. These attribute names are prefixed by “fr” and have a description of “Generic…” in the object configuration screens. For this exercise, we will use an unindexed date (frUnindexedDate1).

Open the Identity Management native console. In the UI, navigate to:
“Configure” → “Managed Objects” → “alpha_user” → “frUnindexedDate1”

image

Change the Readable Title and Description for frUnindexedDate1 to “Password expiration timestamp”. Click Save to go back to the object definition.

Solution Step 2: Create Triggers on the Managed Object (OnCreate and OnUpdate)

On the “[realm]_user” object, click on scripts. Then create OnCreate and OnUpdate scripts.

Both queries will:

  1. Determine if the password is being set/updated.
  2. Query the active password policy for the number of days/weeks/months/years before the password expires.
  3. Calculate the expiration date of the password and store it in the new field.

The OnCreate query looks at the value of object.password and uses this to update the password expiration field.

//----------------------------------------------------//
//-- user.OnCreate()                                --//
//-- Calculate expiration date if password is set   --//
//-- Store value in date field                      --//
//-------------------------------------------------kd-//

if (object.password) {

  //-- Get password policy --//
  passwordPolicy = openidm.read("config/fieldPolicy/alpha_user");
  
  if (passwordPolicy.maxPasswordAge) {

    var passwordPolicyArray = passwordPolicy.maxPasswordAge.split(" ", 2);

    //-- Calculate password expiration date --//
    var date = new Date();
    
    switch(passwordPolicyArray[1]) {
      case "d":
        date.setUTCDate(date.getUTCDate() + parseInt(passwordPolicyArray[0]));
        break;
      case "w":
        date.setUTCDate(date.getUTCDate() + (parseInt(passwordPolicyArray[0]) *7));
        break;  
      case "m":
        date.setUTCMonth(date.getUTCMonth() + parseInt(passwordPolicyArray[0]));
        break;
      case "y":
        date.setUTCFullYear(date.getUTCFullYear() + parseInt(passwordPolicyArray[0]));
        break;
    }

    //-- Set password expiration date --//
    object.frUnindexedDate1 = date.toISOString();
  }
}

The OnUpdate query looks at the value of newObject.password and oldObject.password. If the field values do not match, newObject.password is used to update the password expiration field.

//----------------------------------------------------//
//-- user.OnUpdate()                                --//
//-- Calculate expiration date if password changes  --//
//-- Store value in date field                      --//
//-------------------------------------------------kd-//

if (newObject.password != oldObject.password) {

  //-- Get password policy --//
  passwordPolicy = openidm.read("config/fieldPolicy/alpha_user");

  if (passwordPolicy.maxPasswordAge) {

    var passwordPolicyArray = passwordPolicy.maxPasswordAge.split(" ", 2);

    //-- Calculate password expiration date --//
    var date = new Date();
    
    switch(passwordPolicyArray[1]) {
      case "d":
        date.setUTCDate(date.getUTCDate() + parseInt(passwordPolicyArray[0]));
        break;
      case "w":
        date.setUTCDate(date.getUTCDate() + (parseInt(passwordPolicyArray[0]) *7));
        break;  
      case "m":
        date.setUTCMonth(date.getUTCMonth() + parseInt(passwordPolicyArray[0]));
        break;
      case "y":
        date.setUTCFullYear(date.getUTCFullYear() + parseInt(passwordPolicyArray[0]));
        break;
    }

    //-- Set password expiration date --//
    newObject.frUnindexedDate1 = date.toISOString();
  }
}

Solution Step 3: Deploy Password Expiration Warning into an Authentication Journey

Now that we are storing the password expiration date into a field, we need a few node scripts to compare the current date and expiration date. From there we must have a flexible way to take action on this comparison.

The following is a somewhat simplified journey that will notify the user within a configurable number of days of the expiration date.

The authentication journey:

The flow is as follows:

  1. Do a standard username/password authentication, checking credentials against the LDAP.
  2. Get the expiration date. This implemented here as an inner tree that queries the user object and places the result in the session state.
  3. Check expiration date using a date comparison script.
  4. If within the date expiration period, notify the user and give a chance to reset the password.
  5. If the user choses to change the password, prompt for a new password and patch the object with this value.

The Check Exp Date script:

//----------------------------------------------------//
//-- Check Exp Date                                 --//
//-- Compares the datetime stored in the            --//
//--    frUnindexedDate1 field to the current       --//
//--    datetime.                                   --//
//----------------------------------------------------//
//-- Returns true/false depending if within range.  --//
//-------------------------------------------------kd-//

//-- Set number of days before expiration to start warning --//
var warnDays = 7;

//-- Get number of days left before password expiration and compare --//
var expDate = sharedState.objectAttributes.frUnindexedDate1;
expDays = ((new Date(expDate)).getTime() - new Date().getTime())/(1000*60*60*24)
if (expDays < warnDays) {
  outcome = "Warn";
} else {
  outcome = "OK";
}

That is it… Expiration dates will still function as they did before: After the actual expiration date, the Identity Store Decision node will handle the expiration state as it does out-of-the-box.

Side topic: Retrieving the expiration timestamp in the inner tree

The final thing to mention is that the retrieval of date expiration field in the inner tree. At the time of writing native IDM functions can be called from an AM authN node. This can be queried using OAuth tokens with standard REST calls to the IDM endpoint.

For this implementation, I chose a different method:

  1. Use the identify existing user with the userName as the identity attribute and frUnindexedDate1 as the identifier. This pulls the date into shared state, but also modifies the userName.

  2. To reset the userName, use the identify existing user with the userName as the identity attribute and userName as the identifier in the next step. This keeps both username and the datestamp in shared state and “resets” the userName back to the actual user name.

I am not sure if these two calls are faster/slower than obtaining an OAuth token + calling the IDM endpoint. But, it is easier for this limited demo.

In any case, this inner tree should be replaced when AM journeys can call IDM functions natively. Thanks to the modularity of journeys, changing only this inner tree will be very simple.

4 Likes