AM Scripted Decision Node (Groovy) - Dynamic Choice Collector List

As part of my AM service journey, I wanted to display the user a dynamic choice collector list containing their phone numbers from their billing account contact methods. They may have one phone number OR they may have 10 phone numbers. Once they’ve selected one of the choices I will provide them with different MFA options (BySMS, ByPhoneCall). Just wondering if this is possible?

1 Like

Hi @jfarrell,

I think you’ll find the answer in this blog post: AM Choice Collector Dynamic List

Regards
Patrick

1 Like

hi @jfarrell
You may also use the configuration provider node :point_right: Configuration Provider node :: ForgeRock Identity Cloud Docs
Regards,
Steph.

1 Like

I’m still struggling to see how I can do this!

If in the “queryBillingAcct” scripted node I have a groovy map phoneNumbers that looks like this:

[phoneNumber:5555555555, phoneNumberType:Mobile]
[phoneNumber:6666666666, phoneNumberType:Mobile]
[phoneNumber:8888888888, phoneNumberType:Home]
[phoneNumber:9999999999, phoneNumberType:Home]

How would i then present the “phoneNumber” in a Dynamic Choice Collector?

Hi @jfarrell,

First, with an OOTB Choice Collector, the options dictate the node outcomes. The auth tree structure is static in nature (e.g you can’t really create outcomes for all of the possible phone numbers… and wire them to the next nodes…) - so a Configuration Provider node would probably not make it if this was possible.

So taking this blog AM Choice Collector Dynamic List as a basis, the scripted decision node task is to transform the phone map to an options array like [ “home: 5555555555”, "Mobile: 6666666666, … ] that is rendered to the user via the ChoiceCallback, and then after user selection, store the outcome (a phone number) in shared state, to be used by the next node…

Regards
Patrick

1 Like

I simplified my Scripted Decision Node quite a bit and now have an array stored in sharedState as follows:

DEBUG: newSharedState {realm=/example, authLevel=0, pageNodeCallbacks={0=0, 1=1}, username=testuser, preferredlanguage=en, phoneNumberList=[4444444444, 5555555555, 8888888888, 9999999999]}

Now as you mentioned the task now is to render the phoneNumberList array options to the user via the ChoiceCallback and allow the user to select which phone number. Once the selection is stored in sharedState I can use that variable in the subsequent nodes.

I created a Scripted Decision Node as follows:

import org.forgerock.openam.auth.node.api.Action
import javax.security.auth.callback.ChoiceCallback

// Retrieve the phone numbers array from sharedState
def phoneNumbersArray = sharedState.get(“phoneNumberList”)

if (callbacks.isEmpty()) {
Action.send(
new ChoiceCallback(
“Please select a phone number:”,
phoneNumbersArray as String[],
0,
false
)
).build()
} else {
def index = callbacks.get(0).getSelectedIndexes()[0]
def selectedPhoneNumber = phoneNumbersArray[index]
sharedState.put(“selectedPhoneNumber”, selectedPhoneNumber)
Action.goTo(“true”).build()
}

But when I use this Scripted Decision Node it doesn’t do anything. There are no errors either!

Try :

var fr = JavaImporter(
    org.forgerock.openam.auth.node.api.Action,
    javax.security.auth.callback.ChoiceCallback
)

...


fr.Action.send(
        new fr.ChoiceCallback(
...

fr.Action.goTo(option).build();

Regards
Patrick

Here is the updated Scripted Decision Node in javascript:

// Import Classes
var fr = JavaImporter(
    org.forgerock.openam.auth.node.api.Action,
    javax.security.auth.callback.ChoiceCallback
)

logger.error("2DEBUG phoneListChoiceCallback Scripted Decision Node - BEGIN");
var phoneNumbersArray = sharedState.get("phoneNumberList");
logger.error("2DEBUG phoneNumbersArray: " + phoneNumbersArray);
var phoneNumbersStringArray = [];

// Initialize an empty array and convert each element to a string and store them
for (var i = 0; i < phoneNumbersArray.length; i++) {
    phoneNumbersStringArray[i] = String(phoneNumbersArray[i]);
}
logger.error("2DEBUG phoneNumbersStringArray: " + phoneNumbersStringArray);
             
if (callbacks.isEmpty()) {
    logger.error("2DEBUG inside callbacksisEmpty");
    var choiceCallback = new fr.ChoiceCallback(
        "Please select a phone number:",
        phoneNumbersStringArray,
        0,
        false
    );
    logger.error("2DEBUG choiceCallback created: " + choiceCallback);
    logger.error("2DEBUG choiceCallback prompt: " + choiceCallback.getPrompt());
    
    // Convert the choices array to a string using a JavaScript loop
    var choicesString = "";
    for (var i = 0; i < choiceCallback.getChoices().length; i++) {
        if (i > 0) {
            choicesString += ", ";
        }
        choicesString += choiceCallback.getChoices()[i];
    }
    logger.error("2DEBUG choiceCallback options: " + choicesString);
    
    fr.Action.send(choiceCallback).build();
    logger.error("2DEBUG choiceCallback sent");
    logger.error("2DEBUG outcome is false");
    outcome = "false";
} else {
    logger.error("2DEBUG inside else statement");
    var index = callbacks.get(0).getSelectedIndexes()[0];
    var selectedPhoneNumber = phoneNumbersStringArray[index];
    sharedState.put("selectedPhoneNumber", selectedPhoneNumber);
    logger.error("2DEBUG selectedPhoneNumber - " + selectedPhoneNumber);
    fr.Action.goTo("true").build();
    logger.error("2DEBUG phoneListChoiceCallback Scripted Decision Node - END");
    outcome = "true";
}

The debugging of the code above looks fine, but nothing is presented to the client in the browser:

ERROR: 2DEBUG phoneListChoiceCallback Scripted Decision Node - BEGIN
ERROR: 2DEBUG phoneNumbersArray: [4444444444, 5555555555, 8888888888, 9999999999]
ERROR: 2DEBUG phoneNumbersStringArray: 4444444444, 5555555555, 8888888888, 9999999999
ERROR: 2DEBUG inside callbacksisEmpty
ERROR: 2DEBUG choiceCallback created: javax.security.auth.callback.ChoiceCallback@373aaef7
ERROR: 2DEBUG choiceCallback prompt: Please select a phone number:
ERROR: 2DEBUG choiceCallback options: 4444444444, 5555555555, 8888888888, 9999999999
ERROR: 2DEBUG choiceCallback sent
ERROR: 2DEBUG outcome is false

Although the client browser just hangs when it gets to this point and just kills the session. You do get an “Unknown error. Please contact your Administrator” (500 error). However, the Authentication and OtherLogging logs aren’t showing any errors otherwise.

If callbacks are empty, then the last instruction in this code branch should be sending the callback, e.g: return fr.Action.send(choiceCallback).build();. Here the script just return “false”, that’s not a callback.

Once the user has received the form, filled it and submitted, the script is resumed at the other branch (e.g callback is not empty). Then response is processed, and based on the result, outcome is determined - the outcomes are the outputs of the nodes (those small rounded shapes attached to the right of the node) which are linked to the next nodes in the tree. So if all Ok, then return fr.Action.gotTo("true").build else, return fr.Action.goTo("false").build().

And if outcomes are “true” and “false” then these must be configured for the scripted decision node -check this: Scripted decision node API :: ForgeRock Identity Cloud Docs

There are some examples here (look at the legacy ones): Scripted decision node API :: ForgeRock Identity Cloud Docs

Regards
Patrick

1 Like

This seems to work:

var fr = JavaImporter(
org.forgerock.openam.auth.node.api.Action,
javax.security.auth.callback.ChoiceCallback
);

var phoneNumbersArray = [“4444444444”,“5555555555”,“8888888888”,“9999999999”];

if (callbacks.isEmpty()) {
action = fr.Action.send(
new fr.ChoiceCallback(“Please select a phone number”,phoneNumbersArray,0,false)).build()
} else {
var index = callbacks.get(0).getSelectedIndexes()[0]
var selectedPhoneNumber = phoneNumbersArray[index]
sharedState.put(“selectedPhoneNumber”, selectedPhoneNumber)
action = fr.Action.goTo(“true”).build()
}

Now I just need to change the the script to read the array from sharedState and I should be okay!

Screenshots:
image

image

1 Like