Tuesday 22 September 2015

Data Driven ‘preferences list’ in OpenIDM

My colleague over at idmonsters has done a great job of describing how to add ‘marketing’ preferences to a user’s profile in OpenIDM.  This requirement is fairly typical as it allows user’s (and administrators) to control things like contact preferences as mandated by law.  The method presented at http://idmonsters.blogspot.co.uk/2015/09/implementing-user-marketing-preferences.html uses config files to determine the set of preferences that should be available for management on a user’s profile.  I decided to take this a step further and make this data-driven.  The list of preferences should be populated via REST call.  This goes beyond a fairly standard set of ‘contact preferences’ and allows more flexibility to ask the user for, say, brand preferences, seating preferences, colour choices, disability needs, channel preferences etc.  i.e. the sorts of ‘choices’ where the available list might change based on the changing drivers of the business.

In the approach presented here I used the generic repository object capability of OpenIDM to store the list of preferences.  This is an often overlooked feature that allows any data to be stored in the OpenIDM repository and accessed via a REST call.  These generic objects are not ‘managed objects’ and therefore can’t participate directly in synchronisation mappings.  They also don’t have a user interface in order to define the behaviour, nor a user interface to manage data – it is all done through REST.  You could of course populate generic objects as part of a Master Data Management strategy that sees the master of this data being elsewhere, or maybe the generic object is the master…anyway, I digress…in this scenario the generic object provides a handy way to store and retrieve simple data sets (the ‘preferences’) over REST.

Prepare Generic Repository Object

Populate Data

So, let’s begin by populating the data.  Fire up your favourite REST client and formulate a ‘PUT’ request.  I typically use Postman, but CURL etc will work just as well.  The parameters we’ll use are:
Method: PUT
Headers:
  • X-OpenIDM-Username: <user> (e.g. openidm-admin) 
  • X-OpenIDM-Password: <password> (e.g. openidm-admin) 
  • Content-Type: application/json 
  • If-None-Match: *
Data:
{"name":"London", "type":"location"}
URL: http://openam.example.com/openidm/repo/custom/preferences/london

Just look at the URL for a moment.  The /repo/custom element tells OpenIDM that we’re planning on storing a generic repository object.  The name of the object is the next element i.e. ‘preferences’.  Then the last element is the _id of the item we’re going to store in this object.  (I find it helps to think of the ‘object’ as a table and the items as rows).
The ‘Data’ will be the values stored as the item in the object.  In this case it is essentially two columns: name and type.

Let’s add some more data using the same Method and Headers, but with the following Data and URLs:

Data:
{"name":"Manchester", "type":"location"}
URL: http://openam.example.com/openidm/repo/custom/preferences/manchester

Data:
{"name":"Glasgow", "type":"location"}
URL: http://openam.example.com/openidm/repo/custom/preferences/glasgow

So far we’ve added three preferences of type ‘location’.  You may not need to include the ‘type’ in your model, or you may need more complex Data.  But for now we’ll add more preferences with a different type:

Data:
{"name":"Bus", "type":"transport"}
URL: http://openam.example.com/openidm/repo/custom/preferences/bus

Data:
{"name":"Underground", "type":"transport"}
URL: http://openam.example.com/openidm/repo/custom/preferences/underground

Data:
{"name":"Taxi", "type":"transport"}
URL: http://openam.example.com/openidm/repo/custom/preferences/taxi

When it comes to the User Interface, we’ll display all of these preferences in a single list, but you should be able to extrapolate what’s going on here to provide multiple/hierarchical lists if you desire.

Configure Access to REST endpoints

By default, OpenIDM restricts access to the REST API for generic repository objects to administrators only.  So, having added the data we now need to configure OpenIDM to allow any logged in user to retrieve this list from the REST API as they’ll be calling this when they access the User Interface.
In ‘access.js’ find the section controlling access to the ‘repo’ endpoint and permit the ‘openidm-authorized’ (for user self management) and ‘openidm-reg’ (for user self registration) roles to access it.  Of course, in production, you may want to be more granular in your permissions.  It should now look like:

        // Disallow command action on repo
        {
            "pattern"   : "repo",
            "roles"     : "openidm-admin,openidm-authorized,openidm-reg",
            "methods"   : "*", // default to all methods allowed
            "actions"   : "*", // default to all actions allowed
            "customAuthz" : "disallowCommandAction()"
        },
        {
            "pattern"   : "repo/*",
            "roles"     : "openidm-admin,openidm-authorized,openidm-reg",
            "methods"   : "*", // default to all methods allowed
            "actions"   : "*", // default to all actions allowed
            "customAuthz" : "disallowCommandAction()"
        },

Configure Profile functionality

Configure permissions to allow users to update their Preferences profile attribute 

Also, by default, OpenIDM restricts the list of profile properties that a user can modify themselves.  An admin can get/set any old property put users are restricted to the list defined in access.js.  So whilst we’re in the file let’s allow users to get/set the ‘preferences’ property of their profile:
var allowedPropertiesForManagedUser =   "preferences,userName,password,mail,givenName,sn,telephoneNumber," +
                                        "postalAddress,address2,city,stateProvince,postalCode,country,siteImage," +
                                        "passPhrase,securityAnswer,securityQuestion";
If we were purely using the REST API to manage/retrieve the information about a user, then we could stop here.  But this article is really about configuring the OpenIDM native UI in order to manage these preferences.

Retrieve Preferences from REST endpoint

Now we need to create a PreferencesDelegate.js file.  This will be used by the UI to call the REST API of the ‘preferences’ generic object in order to retrieve the data to show.  The easiest way to do this is make a copy of the ‘RolesDelegate.js’ and edit that.  (I’m going to make all my UI changes in the ‘default’ branch of the structure rather than make use of the ‘extension’ branch.  The recommendation is to use the extension branch to avoid files being overwritten on upgrade but in my case I have made a copy of each of the files I changed in case I need to revert).
So, make a copy of: 

ui/default/enduser/public/org/forgerock/openidm/ui/user/delegates/RoleDelegate.js 
as
ui/default/enduser/public/org/forgerock/openidm/ui/user/delegates/PreferencesDelegate.js 
Edit PreferencesDelegate.js so that the path to the REST endpoint is the ‘preferences’ repository object rather than the ‘roles’ managed object.  There will be four lines to change that should now look like:
define("org/forgerock/openidm/ui/user/delegates/PreferencesDelegate", [
var obj = new AbstractDelegate(constants.host + "/openidm/repo/custom/preferences");
obj.getAllPreferences = function () {
r._id = "repo/custom/preferences/" + r._id;

User Interface Modification 

Now we’ll modify the UI. There are 4 logical areas to change, with each area needing a changes to both an html template as well as javascript files:

  1. AdminUserRegistration: Administrator functionality to register a new user 
  2. AdminUserProfile: Administrator functionality to modify an existing user’s profile 
  3. UserRegistration: User self-registration 
  4. UserProfile: User modification of their own profile 

 We’ll take each one step by step, although you may not need to implement all items depending on your needs.

AdminUserRegistration

JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/admin/users/AdminUserRegistrationView.js

  1. Include the PreferencesDelegate.js file we created earlier.  Add this after the RoleDelegate reference:
    "org/forgerock/openidm/ui/user/delegates/RoleDelegate",
    "org/forgerock/openidm/ui/user/delegates/PreferencesDelegate"
  2. Ensure the delegate reference is passed as a function parameter called preferenceDelegate:
    ], function(AbstractView, validatorsManager, uiUtils, userDelegate, eventManager, constants, conf, router, roleDelegate, preferenceDelegate)
    
  3. In the formSubmit event ensure that any selected preferences are saved. Add this after the ‘data.roles = …’ line:
    data.preferences = this.$el.find("input[name=preferences]:checked").map(function(){return $(this).val();}).get();
    
  4. In the render event, call the preferences REST API (using the delegate) and add it to the ‘data’ object that will be used to populate the UI. Make the event look like this:
            …
            render: function() {
               $.when(
                roleDelegate.getAllRoles(),
                preferenceDelegate.getAllPreferences()
               ).then(_.bind(function (roles, preferences) {
    
                    var managedRoleMap = _.chain(roles.result)
                                          .map(function (r) { return [r._id, r.name || r._id]; })
                                          .object()
                                          .value();
    
                    this.data.roles = _.extend({}, conf.globalData.userRoles, managedRoleMap);
    
                    var preferenceMap = _.chain(preferences.result)
                                          .map(function (r) { return [r._id, r.name || r._id]; })
                                          .object()
                                          .value();
    
                    this.data.preferences = preferenceMap;
    
                    this.parentRender(function() {
                    …
Note that I’m populating preferences solely from the REST endpoint.  If you look at the ‘roles’ block in this file you’ll see that this is merging ‘managed roles’ with roles configured in a configuration file.  Maybe you’ll want some preferences to be managed by configuration, if so you can replicate the ‘extend’ method in the roles functionality.  E.g:
this.data.preferences = _.extend({}, conf.globalData.userPreferences, preferenceMap);

HTML Template:
ui/default/enduser/public/templates/admin/AdminUserRegistrationTemplate.html
The template specifies the layout of the registration page and is easy enough to work out. You need to decide where to put the preferences list…I chose to locate it ‘after’ (i.e. below) the Password validation rules section. So, add the following block:
            <div class="group-field-block" >
                <label class="light align-right">{{t "Preferences"}}</label>
                {{checkbox preferences "preferences"}}
            </div>

Note the {{t “Preferences”}} item.  This controls the label value in the UI.  If you want to make this globalised then follow the format of the other blocks and update the translation.json file.  I’ll leave that as an exercise for the reader! This block uses the ‘checkbox’ helper function to display the supplied object (the 2nd parameter: preferences), using the name and _id of the items with the object, as checkboxes.  The HTML element it creates is given the ‘name’ of the 3rd parameter (“preferences”).  Note that the JS file relies on the HTML element name in order to work out where to populate that data and which data to submit when the profile is saved. When logged in as an administrator, the ‘Add User’ screen should now look like this:
Now if you add a user, selecting preferences, you’ll be able to see the data stored by retrieving the user in a REST call.
e.g.
http://openam.example.com/openidm/managed/user?_queryId=query-all
might return something similar to this:
        {
            "mail": "jim@example.com",
            "sn": "Bean",
            "userName": "jim",
            "stateProvince": "",
            "preferences": [
                "repo/custom/preferences/bus",
                "repo/custom/preferences/london",
                "repo/custom/preferences/taxi"
            ]
        }


AdminUserProfile

Now let’s edit the Administrator’s functionality to manage a user so that it also includes the preferences functionality.

JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/admin/users/AdminUserProfileView.js

  1. Add the PreferencesDelegate.js file as per AdminUserRegistration above
  2. Add the preferenceDelegate function as per AdminUserRegistration above
  3. Add the data.preferences line as per AdminUserRegistration above
  4. Modify the render event as follows:
    render: function(userName, callback) {
                userName = userName[0].toString();
    
                $.when(
                    userDelegate.getForUserName(userName),
                    roleDelegate.getAllRoles(),
                    preferenceDelegate.getAllPreferences()
                ).then(
                    _.bind(function(user, roles, preferences) {
    
                        var managedRoleMap = _.chain(roles.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
    
                        var preferenceMap = _.chain(preferences.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
    
                        this.editedUser = user;
                        this.data.user = user;
                        this.data.roles = _.extend({}, conf.globalData.userRoles, managedRoleMap);
                        this.data.preferences = preferenceMap; 
                     this.data.profileName = user.givenName + ' ' + user.sn;
       …
    
  5. Modify the reloadData event to ensure the UI is able to highlight the preferences already selected against the user’s profile. Add this after the similar roles block:
        _.each(this.editedUser.preferences, _.bind(function(v) {
          this.$el.find("input[name=preferences][value='"+v+"']").prop('checked', true);
        }, this));
    

HTML file:
ui/default/enduser/public/templates/admin/AdminUserProfileTemplate.html

As per the AdminUserRegistration template add a new UI block for the preferences. In this case I created a whole new section after (i.e. below) the Country and State items:
<div class="clearfix">
  <div class="group-field-block col2">
    <label class="light align-right">{{t "Preferences"}}</label>
    {{checkbox preferences "preferences"}}
  </div>
</div>

An Administrator managing a user’s profile should now have a screen that looks like this:


UserRegistration

This allows the user to self-register their own profile.
JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/user/UserRegistrationView.js
  1. Add the PreferencesDelegate.js file as per AdminUserRegistration above
  2. Add the preferenceDelegate function as per AdminUserRegistration above
  3. Modfiy the formSubmit event by adding the data.preferences line:
    … 
    var data = form2js(this.$el.attr("id")), element;
    data.preferences = this.$el.find("input[name=preferences]:checked").map(function(){return $(this).val();}).get();
    delete data.terms;
    …
    
  4. Modify the render event as follows:
            render: function(args, callback) {
    
            $.when(preferenceDelegate.getAllPreferences()
            ).then(
              _.bind(function(preferences){
    
                conf.setProperty("gotoURL", null);
    
                var preferenceMap = _.chain(preferences.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
                this.data.preferences = preferenceMap;
    
                this.parentRender(_.bind(function() {
    
                    …
                    }, this));
                }, this));
              }, this));
            },
    
HTML file:
ui/default/enduser/public/templates/user/UserRegistrationTemplate.html

Add a new UI block for the preferences.  I added this below the fieldset defining the user profile properties:
<fieldset class="fieldset col0">
    <div class="group-field-block">
        <label for="marketing" class="light align-right">Preferences</label>
        <div class="float-left separate-message">
             {{checkbox preferences "preferences"}}
        </div>
     </div>
</fieldset>

Now, having enabled user self-registration (https://backstage.forgerock.com/#!/docs/openidm/3.1.0/integrators-guide#ui-self-registration) the user should see a screen like this:
The user can now self-register, including preferences information.


UserProfile

To allow the user to modify their own profile preferences once they have been registered carry out the following changes.

This requires two JS files updating along with the HTML template
JS file:
ui/default/enduser/public/org/forgerock/commons/ui/user/profile/UserProfileView.js
This is a fairly simple change replicating the formSubmit event handling of the Admin side:

                …
                this.data = form2js(this.el, '.', false);
                this.data.preferences = this.$el.find("input[name=preferences]:checked").map(function(){return $(this).val();}).get();
                
                // buttons will be included in this structure, so remove those.
                                    …
JS file:
ui/default/enduser/public/org/forgerock/openidm/ui/user/profile/UserProfileView.js

This looks like fairly complex set of changes in order to introduce the PreferencesDelegate and call it at the appropriate point in order to populate the preferences data element, as well as select the items in the list that the user has already added to their profile. But, as it turns out, we end up making this file look like the AdminUserProfile to call the getAllPreferences function.

  1. Add the PreferencesDelegate reference as per Step 1 of the Admin side
  2. Add the preferenceDelegate function reference as per Step 2 of the Admin side
  3. Change the obj.render assignment as follows:
    obj.render = function(args, callback) {
         $.when(preferenceDelegate.getAllPreferences()
         ).then(
           _.bind(function(preferences){
                var preferenceMap = _.chain(preferences.result)
                            .map(function (r) { return [r._id, r.name || r._id]; })
                            .object()
                            .value();
                obj.data.preferences = preferenceMap;
    
            if(conf.globalData.userComponent && conf.globalData.userComponent === "repo/internal/user"){
                obj.data.adminUser = true;
            } else {
                obj.data.adminUser = false;
            }
    
            this.parentRender(function() {
                var self = this,
              …
                    });
    
                    this.reloadData();
    
                   _.each(conf.loggedUser.preferences, _.bind(function(v) {
                       this.$el.find("input[name=preferences][value='"+v+"']").prop('checked', true);
                   }, this));
    
                    if(callback) {
                        callback();
                    }
    
                }, this));
            });
           },this));
        };
    
HTML Template:
ui/default/enduser/public/templates/user/UserProfileTemplate.html

Add the same block as per the AdminUserProfile.  I added this after (i.e. below) the {{/if}} section that wraps the Address Details block:
        <div class="clearfix">
            <div class="group-field-block col2" >
                <label class="light align-right">{{t "Preferences"}}</label>
                {{checkbox preferences "preferences"}}
            </div>
        </div>
When the user edits their profile they should get a screen that looks like this:

Summary

You’re done!  You now have a list preferences populated from a REST endpoint (which happens to be an OpenIDM generic repository object) that are selectable by users and admins alike.

Monday 24 August 2015

Windows 2012 R2, ADFS3, WIF4.5 and OpenAM v12

Recently I was involved in a Proof of Concept that required OpenAM v12 to be the authentication service to an ASP.net application that relied on Integrated Windows Authentication.  Normally, you might use the IIS Policy Agent for this scenario because it supports Impersonation.  However, this also requires that OpenAM's DataStore is configured as the Active Directory.  In the PoC, OpenAM was not able to use the AD as the DataStore and was instead depending on an OpenDJ DataStore.  To resolve this I made use of Windows Identity Framework 4.5 as part of Windows 2012 R2 because this has the ability to Impersonate a user based on Windows Identity Claims.  These claims are provided to WIF4.5 via ADFS 3 (it's not formally called ADFS 3 - but it is the version that comes with 2012 R2 that everyone refers to as ADFS 3).  The claims were passed-through ADFS having being initially generated by OpenAM v12.

I decided to write this up as a series of wiki articles providing step-by-step guidance because I realised that some people might come at this knowing Windows/ADFS well, but limited experience with OpenAM.  Similarly, some readers might be very familiar with OpenAM, but have limited Windows experience.  So the series of articles is designed such that you can skip various parts if you already have the necessary components.  Bear in mind that SSL is a 'must' - even for a PoC - so you need to be aware of how to exchange and trust self-signed certificates across the different servers.  In my PoC I used different operating systems (CentOS for the OpenAM instance) so the articles also explain how to exchange self-signed certificates.

The articles can be found here:
https://wikis.forgerock.org/confluence/display/openam/ADFS+3+%28Windows+2012+R2%29+and+OpenAM+12

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!