Implementing Joiner Request in ForgeRock Identity Cloud

Originally posted on https://medium.com/@aydintekin

In certain B2B/B2C use-cases we might need an approval step when users join a certain organisation. This article will describe the use-case and show how such a workflow can be implemented natively in ForgeRock Identity Cloud.

Use case

Company A has created a web portal for their partners to use for their field service operations. To capsulate their permissions, every service partner has their own organisation and one/several key-user(s) who can add new users(employees) to their organisation.
Besides adding users by inviting them, they also want a feature for their employees to directly register and request access for the organisation. An approval step has to be implemented, in which the owner of the organisation can process joiner requests.

Sample workflow for joiner requests

Data Model

Since creating new relationship attributes on FIDC is on the roadmap but not available yet, we will need 2 additional attributes for this constellation to work:

  1. frIndexedString2 has to store the name of the organisation the user wants to join into
  2. frIndexedString3 has the timestamp of the request

This way we can simply project a request for joining an organisation:

Improvement wise you might want to use the ID of the organisation in frIndexedString2 and use frUnindexedDate1 for storing the timestamp. In our case we will keep it simple.

Setup

To create such a workflow in FIDC (ForgeRock Identity Cloud) 3 steps have to be taken; create a journey for the user joiner request, add REST API endpoints for the approver operations and host a modified UI with a view to process requests.

1. Create Journey

In the first step we need to create a journey to enable users to self-register and request for access a certain organisation.
We can simply duplicate the standard registration journey and enhance it with a scripted decision node, so the user has a choice for the organisation he wants to join:

Sample journey with organisation selection for user

In the scripted decision node we will use the following script:
var outcome = "true";
var choices = ["Sales", "IT"];
var fr = JavaImporter(
    org.forgerock.openam.auth.node.api.Action,
    javax.security.auth.callback.ChoiceCallback
);
if (callbacks.isEmpty()) {
    action = fr.Action.send(new fr.ChoiceCallback("Choose your organization", choices, 0, false)).build();
}
else {
    var selectedMode = callbacks.get(0).getSelectedIndexes()[0];
    var selectedChoice = choices[selectedMode];
    if (selectedChoice) {
        var objAttributes = sharedState.get("objectAttributes");
        objAttributes.put("frIndexedString2", selectedChoice);
        objAttributes.put("frIndexedString3", new Date().toString());
        sharedState.put("objAttributes", objAttributes);
    }
}

In the second line (choices variable), you might need to customize/add additional organisations in your FIDC tenant, a better way would be to aggregate them dynamically in the script. We will skip that to keep everything simple and with good performance.

Let’s add that to the decision node and save the journey:


Configuration of the scripted decision node

2. Create REST API Endpoints

After completing the journey, we need REST APIs for the approver to retrieve all open requests and process them.

To add custom API endpoints, we will go to “Scripts” → ”Custom Endpoints” → “New Endpoint”:

Create custom endpoint

The first endpoint should be called “GetJoinerApprovals” and use the following script for it:
let loggedInUser =
  context.parent.parent.parent.parent.parent.rawInfo["user_id"];

// query all orgs the given user is owner of
function queryOrgs(userId) {
  return openidm.query(
    "managed/alpha_organization",
    { _queryFilter: 'ownerIDs co "' + userId + '"' },
    ["_id", "name"]
  ).result;
}

// query all users to wants to join a certain organisation
function queryRequestors(orgName) {
  return openidm.query(
    "managed/alpha_user",
    { _queryFilter: 'frIndexedString2 eq "' + orgName + '"' },
    ["_id", "givenName", "sn", "userName", "frIndexedString3","frIndexedString2"]
  ).result;
}

// return all open requests a user has to decide on
function getRequestors(userId) {
  let orgs = queryOrgs(userId);
  let retRequestors = [];
  orgs.map((o) => {
    let orgRequestors = queryRequestors(o.name);
    orgRequestors.map(or => {
      retRequestors.push(or);
    });
  });
  return retRequestors;
}

(function () {
  if (request.method === "create") {
    return {};
  } else if (request.method === "read") {
    // return all open requests for myself
    return { userid: loggedInUser, results: getRequestors(loggedInUser) };
  } else if (request.method === "update") {
    return {};
  } else if (request.method === "patch") {
    return {};
  } else if (request.method === "delete") {
    return {};
  }
  throw { code: 500, message: "Unknown error" };
})();

This script will look for all the organisations the logged in user is owner of and list all users requesting access for them.

Additionally a REST API is needed for the decision handling of the approver, which we will call “ApproveJoiner”:

let userId = request.additionalParameters.user;
let decision = request.additionalParameters.decision;
let loggedInUser =
  context.parent.parent.parent.parent.parent.rawInfo["user_id"];

// return user by user id
function getUser(userId) {
  return openidm.read("managed/alpha_user/" + userId, null, [
    "_id",
    "frIndexedString2",
  ]);
}

// return the organisation object by name and check if the logged in user is owner of it
function queryOrg(orgName, loggedInUser) {
  let retOrg = openidm.query(
    "managed/alpha_organization",
    { _queryFilter: 'name eq "' + orgName + '" and ownerIDs co "' + loggedInUser + '"' },
    ["_id", "name"]
  ).result;
  return retOrg.length > 0 ? retOrg[0] : null;
}

// clear approval data
function clearUserData(user) {
    return openidm.patch("managed/alpha_user/" + user._id, user._rev, [
        {"operation":"remove","field":"/frIndexedString2"},
        {"operation":"remove","field":"/frIndexedString3"}
      ]);
}

// assign user to org
function assignUserToOrg(org, user) {
  return openidm.patch("managed/alpha_organization/" + org._id, org._rev, [
    {
      operation: "add",
      field: "/members/-",
      value: {
        _ref: "managed/alpha_user/" + user._id,
        _refProperties: {},
      },
    },
  ]);
}

(function () {
  if (request.method === "create") {
    let isApprove = false;
    if (decision && decision.toLowerCase() === "approve") {
        isApprove = true;
    }
    // query user object
    let user = getUser(userId);
    if (user) {
      // query org object + check if user is owner of it
      let org = queryOrg(user.frIndexedString2, loggedInUser);
      if (org) {
        // clear approval data beforehand
        clearUserData(user);
        // if the decision is an approve, assign user to org
        if (isApprove) {
            return assignUserToOrg(org, user);
        } else {
            // otherwise do nothing and return original org
            return org;
        }
      } else {
        return { error: "Couldnt assign org to user" };
      }
    } else {
      return { error: "User not found!" };
    }
  } else if (request.method === "read") {
    return {};
  } else if (request.method === "update") {
    return {};
  } else if (request.method === "patch") {
    return {};
  } else if (request.method === "delete") {
    return {};
  }
  throw { code: 500, message: "Unknown error" };
})();

This script will expect a POST request with 2 parameters, the userid and the decision (approve/decline) for the approval.

3. Host custom platform UI

The enduser UI of FIDC is not capable to present joiner approvals to approvers. For that I enhanced the UI, now we can view and process approvals:


For this to work, we need to clone and run the fork I created here:

Test

Now let’s confirm everything works. We need to create two organisations called “IT” and “Sales”. Additionally we need to setup our key user to be owner of both of them.

For the user registration test, we will use our journey with a dummy user:

And check how if the assignment of the orgainsation works by login to the custom UI and process the request:

---

Artifacts

You can find the customized platform UI here:

The other scripts can be found here:

1 Like