Use case: Extend the default REST API with custom endpoints in ForgeRock Identity Cloud

Use case overview

ForgeRock Identity Cloud is architected on top of REST services, which enables almost all policy configurations and identity operations to be managed through REST API calls. One of Identity Cloud’s most powerful features is the ability to create custom endpoints that can extend the default ForgeRock REST API with new endpoints to perform a wide range of tasks, including those with custom logic.

In this example use case, we’ll demonstrate a custom endpoint by configuring a REST endpoint that invokes a journey to generate a one-time passcode (OTP) on behalf of an end user. This example shows that journeys are, in themselves, REST endpoints.

Steps to achieve this use case

This example use case has two stages:

Create a journey that returns an OTP

First, we’ll create a journey that returns a generated OTP. The journey includes three scripted nodes, with the following scripts:

  • Return OTP
  • OTP valid
  • OTP invalid

Return OTP

Create the following Return OTP script. This script returns a generated OTP using a TextOutputCallback, which represents a callback used to display a message.

/* Return OTP
 *
 * Author: volker.scheuber@forgerock.com
 * 
 * Return the generated OTP using a TextOutputCallback.
 * 
 * This script does not need to be parameterized. It will work properly as is.
 * 
 * The Scripted Decision Node needs the following outcomes defined:
 * - true
 */
outcome = "true";
var fr = JavaImporter(
    org.forgerock.openam.auth.node.api.Action,
    javax.security.auth.callback.TextOutputCallback
)
if (callbacks.isEmpty()) {
    action = fr.Action.send(
        new fr.TextOutputCallback(
            fr.TextOutputCallback.INFORMATION,
            nodeState.get("oneTimePassword").asString()
        )
    ).build()
}
else {
  action = fr.Action.goTo(outcome).build();
}

OTP Valid

Create the following OTP Valid script. This script returns the TextOutputCallback indicating the provided OTP was valid.

/* OTP Valid
 *
 * Author: volker.scheuber@forgerock.com
 * 
 * Return TextOutputCallback indicating the provided OTP was valid.
 * 
 * This script does not need to be parametrized. It will work properly as is.
 * 
 * The Scripted Decision Node needs the following outcomes defined:
 * - true
 */
outcome = "true";
var fr = JavaImporter(
    org.forgerock.openam.auth.node.api.Action,
    javax.security.auth.callback.TextOutputCallback
)
if (callbacks.isEmpty()) {
    action = fr.Action.send(
        new fr.TextOutputCallback(
            fr.TextOutputCallback.INFORMATION,
            "VALID"
        )
    ).build()
}
else {
  action = fr.Action.goTo(outcome).build();
}

OTP Invalid

Create the following OTP Invalid script. This script returns the TextOutputCallback indicating the provided OTP was invalid.

/* OTP Invalid
 *
 * Author: volker.scheuber@forgerock.com
 * 
 * Return TextOutputCallback indicating the provided OTP was invalid.
 * 
 * This script does not need to be parametrized. It will work properly as is.
 * 
 * The Scripted Decision Node needs the following outcomes defined:
 * - true
 */
outcome = "true";
var fr = JavaImporter(
    org.forgerock.openam.auth.node.api.Action,
    javax.security.auth.callback.TextOutputCallback
)
if (callbacks.isEmpty()) {
    action = fr.Action.send(
        new fr.TextOutputCallback(
            fr.TextOutputCallback.INFORMATION,
            "INVALID"
        )
    ).build()
}
else {
  action = fr.Action.goTo(outcome).build();
}

Create the journey

  1. Sign in to the Identity Cloud admin UI using your admin tenant URL, in the format https://<tenant-name>/am/XUI/?realm=/#/.

  2. Go to Journeys > New Journey.

  3. Enter a unique name for the OTP endpoint journey (for example, OTPEndpoint), select which identities will authenticate using this journey, (optionally) enter a journey description, and click Save.

  4. Create a journey similar to this:

    Node descriptions:

    • Anonymous - This Anonymous User Mapping node allows users to log in to an application or website without providing credentials.

    • HOTP Generator - Creates a string of random digits of the specified length for use as a one-time password. See HOTP Generator node for further information.

    • Return OTP - This Scripted Decision node returns the generated OTP. See Step 5 for further information on configuring this node.

    • Validate OTP - This OTP Collector Decision node requests and verifies the returned OTP.

    • OTP Valid - This Scripted Decision node returns a TextOutputCallback indicating the provided OTP was valid. See Step 6 for further information on configuring this node.

    • OTP Invalid - This Scripted Decision node returns a TextOutputCallback indicating the provided OTP was invalid. See Step 7 for further information on configuring this node.

  5. Click on the Return OTP node (Scripted Decision node), select the Return OTP script you created previously, and enter true in the Outcomes.

  6. Click on the OTP Valid node (Scripted Decision node), select the OTP Valid script you created previously, and enter true in the Outcomes.

  7. Click on the OTP Invalid node (Scripted Decision node), select the OTP Invalid script you created previously, and enter true in the Outcomes.

  8. Click Save to save the journey.

Configure a custom endpoint that invokes the OTP journey

  1. In the Identity Cloud admin UI, go to Scripts > Custom Endpoints.

  2. Click New Endpoint.

  3. Enter a name and optional description for the custom endpoint.

  4. Replace the default script with a script similar to the following.

    Replace <journey-name> with the name of your OTP journey, for example, "OTPEndpoint" and <tenant name> with your Identity Cloud tenant.

    //* OTP generation and validation *//
     (function () {
     var journey = "<journey-name>";
     var txId = context.current.parent.parent.parent.headers["x-forgerock-transactionid"][0];
     logger.info("otp: Method: {} txId: {}", request.method, txId);
     if (request.method === 'create') {
       // POST
       var postParams = {
         "url": "https://<tenant-name>/am/json/alpha/authenticate?authIndexType=service&authIndexValue="+journey,
         "method": "POST",
         "headers": {
           "Accept-API-Version": [
             "resource=2.0, protocol=1.0"
           ],
           "Content-Type": [
             "application/json"
           ],
           "Accept": [
             "*/*"
           ],
           "x-forgerock-transactionid": [
             txId
           ]
         },
         "body": '{"authId":"'+request.content.session+'","callbacks":[{"type":"TextOutputCallback","output":[{"name":"message","value":"xxx"},{"name":"messageType","value":"0"}],"_id":0},{"type":"PasswordCallback","output":[{"name":"prompt","value":"One Time Password"}],"input":[{"name":"IDToken3","value":"'+request.content.otp+'"}],"_id":1}]}'
       }
       try {
         var postResult = openidm.action("external/rest", "call", postParams);
         return {
           _id: txId,
           valid: postResult.callbacks[0].output[0].value === 'VALID'
         };
       }
       catch (ex) {
         return {
           _id: txId,
           valid: false
         };
       }
     } else if (request.method === 'read') {
       // GET
       var getParams = {
         "url": "https://<tenant-name>/am/json/alpha/authenticate?authIndexType=service&authIndexValue="+journey,
         "method": "POST",
         "headers": {
           "Accept-API-Version": [
             "resource=2.0, protocol=1.0"
           ],
           "Content-Type": [
             "application/json"
           ]
         }
       };
       var getResult = openidm.action("external/rest", "call", getParams);
       return {
         _id: txId,
         session: getResult.authId,
         otp: getResult.callbacks[0].output[0].value
       };
     } else if (request.method === 'update') {
       // PUT
       return {};
     } else if (request.method === 'patch') {
       return {};
     } else if (request.method === 'delete') {
       return {};
     }
     throw { code: 500, message: 'Unknown error' };
    }());
    

    For example:

  5. Click Save.

    Notice that an example of the URL you are creating is provided under the custom endpoint name. This is the new endpoint that will be exposed, for example, https://<tenant-name>/openidm/endpoint/otp.

To run a test request for the custom endpoint:

  1. Click the triangle icon (Screenshot 2023-04-06 at 14.50.23) to open the Test tab.

  2. Change the method in the test script to "read" and click Run.

    The session data, OTP and user _id are returned in the response, for example:

  3. Copy the response (including curly brackets) and update the test script as follows:

    • Change the method to "create" and add a comma ,.
    • Add "content": and paste the response text.

    For example:

     {
     "request": {
       "method": "create",
    "content":
    {
       "session": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI…OAvHB3Q_W-XIj4DyUU",
      "otp": "816581",
      "_id": "1680709277513-0c373348bd9b11302a56-203808/0"   }
     }
    }
    
  4. Click Run to show the validated token.

Conclusion

This use case has demonstrated how you can configure a custom endpoint in Identity Cloud to call a user journey. You can extend this use case further by integrating customer endpoints into your external UIs or system applications.

Additional resources

Documentation

Training: