AM Choice Collector Dynamic List

I am building out a login journey for an application and have everything working statically. For the users, depending on how / what attributes they have present for MFA, I need to be able to show / hide mfa options based on shared state value I am setting. In a choice collector or configuration node, can I use a shared state value (e.g. telephoneMFA, EmailMFA) to show an email or telephone MFA journey they can select?

For reference, this is the journey I am building. In the set MFA available factors, I have all the values set for ‘what’ they can use.

Want to populate the Choice collector with the sharedState / available factors.

Thanks
Nick

1 Like

Or use a scripted decision node? e.g something like:

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

....

if (callbacks.isEmpty()) {
....
fr.Action.send(
        new fr.ChoiceCallback(
            prompt,
            options,
            0,
            false
        )
    ).build();
} else {
 var index = callbacks.get(0).getSelectedIndexes()[0]
 var option = options.get(index);
 fr.Action.goTo(option).build();
}
2 Likes

Patrick

This is what I was looking for in terms of the options so thanks for getting this reference to me. Now, the next question, how do I do a dynamic list of choices in the ChoiceCallback from this link? I could probably build out a dynamic array and put in the choices object but is more about getting the choice they select in the index value so I know which outcome to publish for the response.

Interactive callbacks :: AM 7.4.0 (forgerock.com)

Thanks
Nick

So I understand the options would be stored in shared state. Same options are thus available on the first iteration (no call back → submit choice callback with those options), and second iteration (filled callback available → resolve outcome from index using the options). I reckon the subtle part is fully dynamic outcomes - which I do not think is possible. So you can only work within the bounds of predictable, fixed outcomes - where some can be dynamically disabled or not. Which makes sense, as a journey itself works within the bounds of predictable, fixed banches.

Regards
Patrick

Yeah, I have the options in the shared state from previous scripting node, really just need a way to hide / not show the options in the callback options list. Not sure if you could style them to hide so they have the same index value for the selection or not, or change the default to a previously selected so is a valid default.

Nick

Hello Nick,

You have the option to differentiate between the number of choices presented to end users and the number of potential outcomes within the scripted decision node.

For instance, within the scripted decision node, you can include all possible outcomes such as:

  1. Email OTP
  2. SMS OTP
  3. Authenticator
  4. Yubikey

However, you can customize the choices displayed to end users by dynamically forming an array, such as:

  1. Email OTP
  2. SMS OTP
  3. Authenticator
    Or
  4. Email OTP
  5. SMS OTP
  6. Yubikey

The subsequent course of action within the node will depend on the choice made by the user.

For example,
below code I have written for confirmation callback.

if (sharedState.containsKey("ismfaenabled")) {
    isMfaEnabled = sharedState.get("ismfaenabled");
    if (isMfaEnabled == "false") {
        ManageMFA.add("Turn On MFA");
    } else {
        ManageMFA.add("Turn Off MFA");
    }
} else {
    ManageMFA.add("Turn On MFA");
}
ManageMFA.add("Switch MFA Method");
ManageMFA.add("Reset MFA Device");
ManageMFA.add("Cancel");
if (callbacks.isEmpty()) {
    action = Action.send(
        new TextOutputCallback(TextOutputCallback.INFORMATION, "What would you like to do today?"),
        new ConfirmationCallback(ConfirmationCallback.INFORMATION, ManageMFA.toArray(new String[ManageMFA.size()]), 1)).build();
    //sharedState.put(mfaAction, ManageMFA[callbacks[1].getSelectedIndex()]);
}

and then scripted decision I have set up all the possible outcomes

2 Likes

Kannan

This is what i was looking for, thanks for getting this over to me.

Thanks
Nick

Kannan

I have this pretty much all done, but running into this error when trying to run it. Is probably something simple I am missing, but any help you can point me to on this would be appreciated…

TypeError: Cannot find function toArray in object Email Link,Email OTP Code

Here is my script block I am using…

//Debug / dump the existing shared state from the node to make sure have the right variables to be used in the script
logger.error("*****STARTING THE MFA SELECTOR SCRIPT************")
logger.error("******MFA SCRIPTED SHARED STATE************: " + sharedState.toString())

//imports
var fr = JavaImporter(
  javax.security.auth.callback.TextOutputCallback,
  javax.security.auth.callback.ConfirmationCallback,
  org.forgerock.openam.auth.node.api.Action
  );

//variable declarations
var oathMFA;
var teleMFA;
var mailMFA;
var mfaOptions = [];
//get the existing shared state values for the MFA options
// oath MFA / TOTP
if (sharedState.containsKey("oathMFA")) {
  oathMFA = sharedState.get("oathMFA");
  logger.error("OATH MFA: " + oathMFA);
  if (oathMFA === true) {
    mfaOptions.add("Authenticator App");
  } else {
    logger.error("oath MFA is not enabled for the user")
  }
}

if (sharedState.containsKey("teleMFA")) {
  teleMFA = sharedState.get("teleMFA");
  logger.error("TELEPHONE MFA: " + teleMFA);
  if (teleMFA === true) {
    mfaOptions.add("SMS OTP")
  } else {
    logger.error("telephone MFA is not enabled for the user")
  }
}

if (sharedState.containsKey("mailMFA")) {
  mailMFA = sharedState.get("mailMFA");
  logger.error("EMAIL MFA: " + mailMFA);
  if (mailMFA === true) {
    mfaOptions.push("Email Link");
    mfaOptions.push("Email OTP Code");
  } else {
    logger.error("Email mfa is not enabled for the user")
  }
}

//setup the return / callback
if (callbacks.isEmpty()) {
 	action = fr.Action.send(
      	new fr.TextOutputCallback(fr.TextOutputCallback.INFORMATION, "Which MFA option would you like to use to authenticate?"),
      	new fr.ConfirmationCallback(fr.ConfirmationCallback.INFORMATION, mfaOptions.toArray(new String[mfaOptions.size()]), 1)).build();
}

//outcome = true;

Looks like this part of this code throwing the error.

  logger.error("EMAIL MFA: " + mailMFA);
  if (mailMFA === true) {
    mfaOptions.push("Email Link");
    mfaOptions.push("Email OTP Code");

instead of .push try .add

    mfaOptions.add("Email Link");
    mfaOptions.add("Email OTP Code");

This one gives me the below…had changed to the ‘push’ earlier given the object types and cannot choose a ‘Set()’ for the mfaOptions object.

TypeError: Cannot find function add in object

I am getting the error on the line below for the toArray method…

new fr.ConfirmationCallback(fr.ConfirmationCallback.INFORMATION, mfaOptions.toArray(new String[mfaOptions.size()]), 1)).build();

mfaOptions is defined as [], therefore it is a javascript array. So yes, add, toArray are not included in the array prototype.

The Confirmation callback expect a Java Array for the options. So something like this would work:

var fr = JavaImporter(
  javax.security.auth.callback.TextOutputCallback,
  javax.security.auth.callback.ConfirmationCallback,
  org.forgerock.openam.auth.node.api.Action,
  java.util.ArrayList,
  );

var mfaOptions = new fr.ArrayList()

Then #add and #toArray are valid, but not #push.

Note that instantiating directly a Java Array requires using the reflection API as per Scripting Java - Rhino

Regards
Patrick

1 Like

This code I shared earlier was written in Groovy. You are working with JavaScript and have defined mfaOptions as an array. so update the code something similar to this.

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

var mfaOptions = [];
mfaOptions.push("SMSOTP");
mfaOptions.push("EmailOTP");
if (callbacks.isEmpty()) {
    action = fr.Action.send(
        new fr.TextOutputCallback(fr.TextOutputCallback.INFORMATION, "Which MFA option would you like to use to authenticate?"),
        new fr.ConfirmationCallback(fr.ConfirmationCallback.INFORMATION, mfaOptions, 1)
    ).build();
}else{
//add relevant code here
}
2 Likes

Thanks both kannan and patrick, was able to get all this working / pushing the right outcomes.

Thanks
nick

1 Like

We have a similar use case but instead of creating separate Confirmation Callback buttons, we would like to use a Boolean radio button. Would the BooleanAttributeInputCallback also work in replacement of the Confirmation Callback?

1 Like

The Boolean Attribute Input Callback is supported in the hosted login pages on ForgeRock Identity Cloud [Interactive callbacks :: ForgeRock Identity Cloud Docs] or in the platform login UI - if you use one of those - otherwise it’s up to the (custom) front end to render it as it sees fit.

Regards
Patrick