Friday 3 July 2015

Gathering user input in a scripted OpenAM Authentication Module [UPDATED for OpenAM v13 28th April 2016]

Overview


For those that know the scripted authentication module capabilities of OpenAM you'll be aware that it is really aimed at gathering data on the client without requiring user input. This a great for situations such as the Device Match module which runs a script to gather data about the device that is accessing the OpenAM authentication services. However, if you wanted to get a user to input data then you really need to consider building a custom authentication module that leverages the Callbacks capability.

Sometimes, you need a simple way to get user input without resorting to a custom module. The approaches I'll show you here are great for that. But, as they don't make use of the Callbacks capability, they won't work well in a RESTful scenario. Therefore I'd limit the use of these approaches to proof-of-concept style deployments.

Approaches

There are two ways I’ve succeeded in getting user input for scripted Authentication Modules. The first is quite clunky, but really quick and really dirty if you just want a simple single-shot user input. The second is more complex, but works with the existing XUI based user interface and is as a flexible as you need it to be.
First lets state that I’m not really interested (in this article) what the server-side script is doing. For simple testing we’ll have a server-side script that assumes the contents of the ‘clientScriptOutputData’ variable is the ‘username’ of a user in the DataStore. Actually, we’ll do a simple validation of this fact before returning SUCCESS of FAILED ‘authStates’ accordingly. I’m not going to expand on what those things mean…the Developer Guide does that for me.  This is a Groovy script:

So, the server-side script (v12) is:
username=clientScriptOutputData
if (!idRepository.getAttribute(username,"uid")) {
  authState = FAILED
} else {
  authState = SUCCESS
}

[UPDATE]
For v13 I'm now using Javascript at the server and getAttribute always returns a 'set'. Therefore we need to use the isEmpty() method as below.  Also note that I am now parsing the clientScriptOutputData variable as JSON.  That's because I've enhanced the client-side script to send the data as JSON - you'll see that later!
data=JSON.parse(clientScriptOutputData);
username=data.username
if (idRepository.getAttribute(username,"uid").isEmpty()) {
  authState = FAILED
} else {
  authState = SUCCESS
}


Now the client side….

Option 1 – Prompt box

I told you it was clunky. But this approach uses the Javscript ‘prompt’ function to generate a dialog box to ask for user input. It’s dead simple. The script is this:
var strUsername=window.prompt("Enter Username","");
output.value=strUsername;
submit()

[UPDATE]
For v13, use:
output.value="{\"username\":\"" + strUsername + "\"}";

Yep, that’s it!

The ‘output’ variable is actually a parameter to a function that wraps this script. The scripted authentication module wraps whatever you put in the ‘client-side’ script into a self-submitting function with ‘output’ as a parameter. The function also defines the ‘submit’ function, which, you’ll not be surprised to hear, submits an HTML form back to the server. The HTML form is generated automatically by the scripted authn module. It contains a few things:

  1. A hidden input element called ‘clientScriptOutputData’. If you’ve been paying attention you’ll notice this is what the server-side script receives from the client. Also, the self-submitting function passes this element to the ‘output’ parameter.
  2. A hidden button which is not named, but has a type=”submit” attribute. This is important because the submit function (defined within the self-submitting function) uses JQuery to find that button (by using the type=”submit” attribute in its search). It then sends the ‘click’ message to the button which submits the form to the server.


Now you can define this in a scripted authentication module called, say, ‘UIDemo’, and then browse to it like this:
<openam_server:port>/openam/XUI/#login/&module=UIDemo

You’ll get the Javascript ‘prompt’ popup. If you enter ‘demo’ (assuming the default demo OpenAM user exists) and click Ok you’ll successfully login and see the ‘demo’ user profile page. If you try a username that does not exist in the DataStore you’ll get an ‘authentication failed’ message.

If you’ve done it correctly it should look a little like this:


Option 2 – Form manipulation

As mentioned in Option 1, the Scripted module creates an HTML form that gets submitted to the server through the self-submitting function. We can use HTML DOM manipulation to add elements to the form, making them appear in the UI, as well as delay the self-submission allowing data entry to take place. However, by doing this we are no longer entirely operating within the context of the self-submitting function so do not have access to the ‘output’ parameter or the ‘submit’ function. No matter, we can still create our own button that populates the ‘clientScriptOutputData’ element and forwards the click to the hidden submit button. If we don’t click our button soon enough then the self-submission delay will end and the form will be submitted with whatever data is contained in ‘clientScriptOutputData’.

The full script (v12) is:
spinner.hideSpinner();
autoSubmitDelay = 30000;
$(document).ready(function(){ 
  fs = $(document.forms[0]).find("fieldset");
  strUI='<div class="group-field-block"><label class="short">Username:</label><input type="text" name="uname"/></div><div class="field field-submit"><input name="submit" id="Submit" class="button" type="button" value="Submit" onclick="document.forms[0].elements[\'clientScriptOutputData\'].value=document.forms[0].elements[\'uname\'].value;$(\'input[type=submit]\').trigger(\'click\');" /></div>';
  $(fs).append(strUI);
});

In this script there is really only 1 line of interest (albeit quite long!) – line 5.
Anyway, let’s take it line by line…

spinner.hideSpinner();
This simply hides the swirly spinner thingy. Your choice if you do this or not!

autoSubmitDelay = 30000;
This tells the self-submitting function to delay self-submission for 30s. You could go longer or shorter…up to you. Maybe someone can suggest a way of disabling self-submission entirely?

$(document).ready(function(){

});
As we’re going to use JQuery, we better wait until JQuery is ready for us.

  fs = $(document.forms[0]).find("fieldset");
Here we use JQuery to find the ‘fieldset’ element inside the HTML form defined by the module. This is because we are going to insert some form elements at this point.

  strUI='<div class="group-field-block"><label class="short">Username:</label><input type="text" name="uname"/></div><div class="field field-submit"><input name="submit" id="Submit" class="button" type="button" value="Submit" onclick="document.forms[0].elements[\'clientScriptOutputData\'].value=document.forms[0].elements[\'uname\'].value;$(\'input[type=submit]\').trigger(\'click\');" /></div>';
Here we build a string that defines the HTML elements we intend to add to the form. We’ll analyse this in more detail in a moment.

  $(fs).append(strUI);
Use Jquery to append the string with the form elements to the fieldset element we found earlier.


Let’s come back to strUI on line 5.
We’re using the classes defined by the XUI interface to create HTML elements. The first ‘div’ is a label and input box (called ‘uname’ in this case).

If we wanted we could add as many fields as we wished (visible or hidden), using the classes the XUI interface adopts which allows us to do validation too.

The second ‘div’ is the button we wish to display with its complex ‘onclick’ event. This event performs two functions:

  1. The first is to populate the ‘clientScriptOutputData’ (hidden) element with the data we wish to submit to the server. In this case it is a simple copy of the ‘uname’ element value. However, with multiple fields or complex data capture requirements the population of ‘clientScriptOutputData’ becomes more complex. You may consider putting this complexity in a function. However, the only place you can define a function is inside this script. This script gets run once when the page loads. Any function defined here is only available when the script is running, and is not available to the page when the user eventually clicks the button. Maybe some UI developers can correct me, but it therefore seems that the onclick event must include, directly, all the logic required to populate the ‘clientScriptOutputData’ element.
  2. The second uses JQuery to trigger the ‘click’ message of the hidden submit button. We can’t use the ‘submit’ function we did in the ‘prompt’ option as the submit function is defined in the wrapper script, which is only available when it runs at page load – not subsequently. This JQuery mechanism to issue the ‘click’ message is actually all that the ‘submit’ function does (if it detects that JQuery is available) so it’s no great shakes to simply replicate that step here.


If you did it correctly it should look a little like this:

[UPDATE]
In v13 the principle is the same but the UI elements have changed slightly.  The key difference is that a 'Log In' button is already available so we no longer want to define our own 'Submit' button which means we need to find an alternative to using the 'onclick' event of that button.  Instead we'll use the 'onchange' event of the input field in order to build a JSON object that is string represented in the hidden 'clientScriptOutputData' field.  This mechanism can be used to easily add additional fields if you so wish (and pairs with the JSON parsing in the server-side script).  So the UI we now build we want to 'prepend' to the form (so that it appears before the provided 'Log In' button) and it shall take account of some CSS differences.  The new client-side script is below. If you've followed the steps for v12 above then this needs no further discussion:

spinner.hideSpinner();
autoSubmitDelay = 30000;
$(document).ready(function(){
  fs = $(document.forms[0]).find("fieldset");
  strUI='<div class="form-group"><label class="aria-label sr-only separator" for="uname">Username</label><input onchange="s=$(\'#clientScriptOutputData\')[0]; if (!s.value) s.value=\'{}\'; d=JSON.parse(s.value); d[\'username\']=value; s.value=JSON.stringify(d);" id="uname" class="form-control input-lg" type="text" placeholder="User Name" value="" name="uname"></input></div>';
  $(fs).prepend(strUI);
});

The page should now look like this:

Summary

So there you have it. Not one, but two ways to retrieve user input in a scripted Authentication Module! I’m sure there are experts out there who could refine, simplify, enhance etc…but the purpose of this was to show that some things that you were thought were impossible or hard, are quite simple once you know what to do!

1 comment:

  1. Hello, i'm trying to fill in values into inputs from external script.
    But i'm stuck on problem, than when i try to select one of these inputs by id/name or anything else, it's returning null.

    ReplyDelete