Two-Step adapter authentication
improve this page | report issueOverview
This tutorial demonstrates how to implement "Two-Step" adapter-based authentication.
Two-Step means that after the initial authentication that uses, for example, a username and a password, an additional authentication step is required, such as a login pin, a secret word, or similar identification. In this example, a secret word is implemented for the second authentication step. The code snippets and sample application in this tutorial are based on the existing adapter-based authentication sample. The changes extend the application from single-step to Two-Step.
Session-independent mode
By default, MobileFirst Platform Foundation 7.1 applications run in a session-independent mode, meaning that you can no longer use HTTP sessions or global variables to persist data across requests. Instead, MobileFirst apps must use a third-party database to store applicative states.
To learn more about the session-independent mode, see its topic in the user documentation.
To demonstrate how to store user data, the tutorial uses the WL.Server.getClientId
API and a Cloudant database.
Agenda
- Prerequisite - Creating an IBM Cloudant account
- Configuring the authenticationConfig.xml file
- Creating the server-side authentication components
- Creating the client-side authentication components
- Sample application
Prerequisite - Creating an IBM Cloudant account
This sample uses IBM Cloudant Database to save user data. To run the sample and understand how to work with Cloudant, first sign up for a free account and create a database.
Then proceed as follows:
- Change the database permissions - Follow the instructions in the Changing Database Permissions tutorial.
- Basic authentication - The basic authentication value is passed as part of every request to the database. Instead of using your username and password to identify, use base-64 encoding to generate a string that is created by concatenating the API
key
andpassword
, separated by a column character in the following manner:key:password
. You use it later to send requests to the database.
For more information, read the Cloudant Basic Authentication documentation.
Configuring the authenticationConfig.xml file
Realms
Add a realm or replace the existing AuthLoginModule
realm in the realms
section of the authenticationConfig.xml
file:
<realm loginModule="AuthLoginModule" name="TwoStepAuthRealm">
<className>com.worklight.integration.auth.AdapterAuthenticator</className>
<parameter name="login-function" value="AuthAdapter.onAuthRequired"/>
<parameter name="logout-function" value="AuthAdapter.onLogout"/>
</realm>
Security tests
Add a security test or replace the existing AuthSecurityTest
in the securityTests
section of the authenticationConfig.xml
file:
<customSecurityTest name="TwoStepAuthAdapter-securityTest">
<test isInternalUserID="true" realm="TwoStepAuthRealm"/><
</customSecurityTest><br />
To review the remaining/existing sample components, see the Adapter-based authentication tutorial.
Creating the server-side authentication components
To put in place the Two-Step authentication process, several changes are necessary to the adapter file (whether XML or JavaScript) and to the database.
Adapter XML file
Edit the AuthAdapter.xml
file:
- Change the domain name to your Cloudant domain:
<domain>$USERNAME.cloudant.com</domain>
- Add the following procedure:
<procedure name="submitAuthenticationStep2" securityTest="wl_unprotected"/>
- Protect the
getSecretData
method with the newTwoStepAuthAdapter-securityTest
Adapter JavaScript file
Edit the AuthAdapter-impl.js
file:
- Create a variable to save the basic authentication encoded string you have generated before:
var auth = "Basic REPLASE_ME_WITH_THE_BASE-64_ENCODED_STRING";
- Create a variable to save your database name:
var dbName = "REPLACE_ME_WITH_THE_DATABASE_NAME";
- Update the
onAuthRequired
function to return that authentication step 1 is required:
function onAuthRequired(headers, errorMessage){ errorMessage = errorMessage ? errorMessage : null; return { authRequired: true, authStep: 1, errorMessage: errorMessage }; }
- Update the
submitAuthenticationStep1
function:- Add the following line to get the client ID:
function submitAuthenticationStep1(username, password){ if (username === "user" && password === "password"){ WL.Logger.debug("Step 1 :: SUCCESS"); var clientId = WL.Server.getClientId(); var userIdentity = { userId: username, displayName: username, attributes: {} };
- To save the
userIdentity
for the next authentication step, write it to the database. Use theclientId
variable as the document_id
key:
//Validate that the DB doesn't already contains the ClientId var response = deleteUserIdentityFromDB(dbName, null); //Write ClientId to DB var response = writeUserIdentityToDB(dbName, {_id:clientId, "userIdentity":userIdentity});
- If step 1 authentication was successful, return that step 2 is required:
if (response){ return { authRequired: true, authStep: 2, question: "What is your pet's name?", errorMessage : "" }; } else { return onAuthRequired(null, "Database ERROR"); } } else{ WL.Logger.debug("Step 1 :: FAILURE"); return onAuthRequired(null, "Invalid login credentials"); } }
- Add the following line to get the client ID:
- Add
submitAuthenticationStep2
function to handle the second authentication step:- Get the client ID and read it from the database:
function submitAuthenticationStep2(answer){ var clientId = WL.Server.getClientId(); var response = readUserIdentityFromDB(dbName, clientId);
- If step 2 authentication was successful, delete the client document from database:
if (response){ if (answer === "Lassie"){ var doc = JSON.parse(response.text); var userIdentity = doc.userIdentity; WL.Logger.debug("Step 2 :: SUCCESS"); WL.Server.setActiveUser("TwoStepAuthRealm", userIdentity); WL.Logger.debug("Authorized access granted"); var response = deleteUserIdentityFromDB(dbName, doc); return { authRequired: false }; } else{ WL.Logger.debug("Step 2 :: FAILURE"); return onAuthRequired(null, "Wrong security question answer"); } } else { WL.Logger.debug("Step 1 :: FAILURE"); return onAuthRequired(null, "Database ERROR"); } }
- Get the client ID and read it from the database:
Database actions
To handle the database actions, use the WL.Server.invokeHttp
method and Cloudant REST API.
- Write to the database:
function writeUserIdentityToDB(db, document){ var input = { method : 'post', returnedContentType : 'plain', path : db, headers: { "Authorization":auth }, body:{ contentType:'application/json; charset=UTF-8', content:JSON.stringify(document) } }; var response = WL.Server.invokeHttp(input); var responseString = "" + response.statusCode; //Checking if the invocation was successful - status code = 2xx if (responseString.indexOf('2') === 0){ return response; } return null; }
- Read from database:
function readUserIdentityFromDB(db, key){ var input = { method : 'get', returnedContentType : 'plain', path : db + "/" + key, headers: { "Authorization":auth } }; var response = WL.Server.invokeHttp(input); var responseString = "" + response.statusCode;</p> //Checking if the invocation was successful - status code = 2xx if (responseString.indexOf('2') === 0){ return response; } return null; }
- Delete from the database:
function deleteUserIdentityFromDB(db, document){ var doc = document; if (!doc){ var clientId = WL.Server.getClientId(); var response = readUserIdentityFromDB(dbName, clientId); if(!response){ return; } else { doc = JSON.parse(response.text); } } var id = doc._id; // The id of the doc to remove var rev = doc._rev; // The rev of the doc to remove var input = { method : 'delete', returnedContentType : 'plain', path : db + "/" + id + "?rev=" + rev, headers: { "Authorization":auth } }; return WL.Server.invokeHttp(input); }
To learn more about IBM Cloudant REST API, see the Cloudant documentation.
Creating the client-side authentication components
- In
index.html
, use theTwoStepAuthRealm
instead of the existing realm:<div id="AppDiv"> ... <input type="button" class="appButton" value="Logout" onclick="WL.Client.logout('TwoStepAuthRealm', {onSuccess:WL.Client.reloadApp})" /> <div id="ResponseDiv"></div> </div>
- Add a second authentication screen:
<div id="AuthStep2Div"> <h3>Authentication Step 2</h3> <p id="AuthQuestion"></p> <input type="text" placeholder="Enter answer" id="AuthAnswer"/><br /> <input type="button" class="formButton" value="Submit" id="AuthStep2Submit" /><input type="button" class="AuthCancelButton" value="Cancel" /> </div>
- Finally, update the challenge handler accordingly.
In this example, a new challenge handler (a new.js
file), calledTwoStepAuthRealmChallengeProcessor.js
, is created for this purpose.- The response is checked as in the original sample application:
var TwoStepAuthRealmChallengeHandler = WL.Client.createChallengeHandler("TwoStepAuthRealm"); TwoStepAuthRealmChallengeHandler.isCustomResponse = function(response) { if (!response || !response.responseJSON || response.responseText === null) { return false; } if (typeof(response.responseJSON.authRequired) !== 'undefined'){ return true; } else { return false; } };
- Add another case for the second authentication step:
TwoStepAuthRealmChallengeHandler.handleChallenge = function(response){ var authRequired = response.responseJSON.authRequired;</p> if (authRequired == true){ $("#AppDiv").hide(); $("#AuthDiv").show(); $("#AuthInfo").empty(); $("#AuthStep1Div").hide(); $("#AuthStep2Div").hide(); switch (response.responseJSON.authStep) { case 1: $("#AuthStep1Div").show(); $("#AuthPassword").val(''); break; case 2: $("#AuthStep2Div").show(); $("#AuthAnswer").val(''); $("#AuthQuestion").html(response.responseJSON.question); break; } if (response.responseJSON.errorMessage) $("#AuthInfo").html(response.responseJSON.errorMessage); } else if (authRequired == false){ $("#AppDiv").show(); $("#AuthDiv").hide(); TwoStepAuthRealmChallengeHandler.submitSuccess(); } };
- Perform the second authentication step:
$("#AuthStep1Submit").bind('click', function () { var username = $("#AuthUsername").val(); var password = $("#AuthPassword").val(); var invocationData = { adapter : "AuthAdapter", procedure : "submitAuthenticationStep1", parameters : [ username, password ] }; TwoStepAuthRealmChallengeHandler.submitAdapterAuthentication(invocationData, {}); }); $("#AuthStep2Submit").bind('click', function () { var answer = $("#AuthAnswer").val(); var invocationData = { adapter : "AuthAdapter", procedure : "submitAuthenticationStep2", parameters : [ answer ] }; TwoStepAuthRealmChallengeHandler.submitAdapterAuthentication(invocationData, {}); }); $(".AuthCancelButton").bind('click', function () { $("#AppDiv").show(); $("#AuthDiv").hide(); TwoStepAuthRealmChallengeHandler.submitFailure(); });
- The response is checked as in the original sample application:
To review the remaining/existing sample client-side implementation, see the Adapter-based authentication in hybrid applications tutorial.
Sample application
Click to download the sample application.
▲Inclusive terminology note: The Mobile First Platform team is making changes to support the IBM® initiative to replace racially biased and other discriminatory language in our code and content with more inclusive language. While IBM values the use of inclusive language, terms that are outside of IBM's direct influence are sometimes required for the sake of maintaining user understanding. As other industry leaders join IBM in embracing the use of inclusive language, IBM will continue to update the documentation to reflect those changes.