IBM Support

Creating Authentication Plugins for IBM Content Navigator for Microsoft Office and IBM Sync for Windows

Question & Answer


Question

How can IBM Content Navigator for Microsoft Office and IBM Navigator Sync for Windows be customized for other authentication providers?

Answer

Today IBM Navigator for Microsoft Office (NMO) and IBM Navigator Sync (Sync) only support Kerberos for SSO authentication. As more customers deploy Navigator onto systems with various authentication schemes, they also want to deploy NMO and Sync and have it authenticate like the web client. Since neither NMO nor Sync run in a browser, special code needs to be added to handle certain scenarios that are otherwise handled automatically in a browser based application. For example, with form based authentication, when a user tries to open IBM Content Navigator (ICN) in the browser, the request is redirected to a login form, where the user enters their credentials. After logging in, they are redirected to Navigator and can continue on.

With NMO and Sync, if the login form is returned in the response, it cannot render the form. In order to authenticate it has to submit the form with the credentials filled in. Since the form can vary from customer to customer, this can be a difficult process to make work for all cases.

With the 2.0.3.4 release, the ability to write a customized plugin dll that handles authentication was added to NMO and Sync for Windows (the Mac Sync client is not included). The plugin provides an interface that allows customers to provide code that inspects every HTTP request/response sent to the ICN server. The plugin would check if the user needs to authenticate, for example by checking if a login form was in the response, and then post the credentials to the authentication server. The actual steps for authenticating are dependent on the configuration.

Plugin Interface

The plugin interface is defined below. It includes just one method, Authenticate(). The Authenticate method would inspect the request and response objects to determine if authentication is required, and if so use the supplied credentials.

public interface AuthenticationPluginInterface


{
/// <summary>
/// This method is called to determine whether authentication needs to occur by
///inspecting the request, response, response exception, etc.
/// and if necessary perform the authentication,
///using the passed in username and password
/// </summary>
AuthenticationStatus Authenticate(
HttpWebRequest webRequest,
HttpWebResponse webResponse,
Exception requestException,
string responseStr,
string userName,
string password);

/// This property should return a displayable name for the plug-in
string Name { get; }
}

The arguments for the Authenticate method are:
HttpWebRequest webRequest
The current HttpWebRequest that was sent to the Navigator server.

HttpWebResponse webResponse
The HttpWebResponse to the web request.

Exception requestException
If the webRequest resulted in an exception, the exception object is passed in here.

string responseStr
If the webRequest was successful, the contents of the request is in the responseStr. If there was an exception then this is null.

AuthenticationStatus is an enum that includes status values indicating the success of the login:

public enum AuthenticationStatus
{
NONE = 0,
AUTHENTICATED,
AUTHENTICATION_FAILED,
SESSION_EXPIRED
}

Creating the Plugin

Do the following to create an authentication plugin, below is some sample code that could be used as a guide. The steps are for NMO, but for sync it is almost exactly the same, other than referencing the files in the sync install directory instead of NMO. The steps assume some familiarity with using Microsoft Visual Studio 2013:

-Create a new project in Microsoft Visual Studio, the type should be a Windows class library

-Make sure the project targets the same .Net version that NMO is using. At the time of this writing, that should be .Net 4.5.2

-Add a reference to “NexusNet.dll”. This file can be found in the NMO install directory.

-If you want to use the same diagnostic logging framework as NMO, then also add a reference to “log4net.dll”. This file can also be found in the NMO install directory.

-Implement the Authenticate() method (see example code below)

Sample Code

The following sample demonstrates using the Authenticate method to check for a login page, and if found post the credentials to the form.

-The credentials supplied are the credentials that are entered in the NMO login dialog. If they are empty then that means the request occurred before the login dialog was shown (NMO tries to connect when it starts up so you will see a few requests before the login dialog is shown).

-The request that posts the credentials uses the same cookie container that was passed into the webRequest object. It is important to use the existing cookie container so that subsequent NMO requests use the resulting LTPA token.

-For windows sync, the IBM namespaces are slightly different, they are:

using IBM.ECM.NavigatorNet.Plugin;

using IBM.ECM.NavigatorNet.Util;

using System;


using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using IBM.ECM.Navigator.API.Utilities;
using IBM.ECM.Navigator.API.Plugin;
using IBM.ECM.Navigator.API;

namespace AuthenticationSample
{
/*
* This code is intended to provide an example for a custom authentication plugin for Navigator for Microsoft Office. It is only
* intended to be used as a sample and must be modified in order to work in a particular deployment.
*
* Requirements to build this project:
* -it must be loaded into Visual Studio (it was created using VS2013)
* -target .Net 4.5.2
* -add the references:
* -NexusNet (NexusNet.dll in the NMO install directory)
* -log4net (if you want to use the existing logging code, you can use the log4net.dll in the NMO install directory)
*
* The main method in this project is the Authenticate method. This method is called after every call is made to the Navigator
* server. The HttpWebRequest and HttpWebResponse or Exception (if any) is passed into this method. Also the HTTP Response
* saved to a string, if there was one, and the user's credentials (if present) are passed in as well.
*
* In general the Authenticate method should:
* -Examine the response or exception to determine if authentication is needed. This could mean checking the response string for
* certain HTML artifacts, such as a login form name, checking for specific errors in the exception, etc.
* -return AuthenticationStatus.NONE if nothing was detected (the call likely occurred after successful authentication)
* -if authentication is needed, check if the credentials were passed in. If they are empty then return AuthenticationStatus.NONE
* as that indicates the call occurred before the user was prompted for login
* -if the credentials are present, perform whatever actions are needed to authenticate. Usually this involves posting the credentials
* to a form
* -if possible, check for any errors in the response, if detected return AuthenticationStatus.AUTHENTICATION_FAILED
* -if successful, return AuthenticationStatus.AUTHENTICATED
* -there is also a AuthenticationStatus.SESSION_EXPIRED, which can be returned to indicate session expiration. Certain systems
* indicate session expiration by a specific error code or cookie. Often the Navigator server will detect session expiration so
* it is not always necessary or possible to detect it here, so it is not required to check for it. Whether or not it should
* be done is dependent on configuration.
*
*/

public class AuthenticationSample : AuthenticationPluginInterface
{
// this is the name of the login page to check for in order to determine if the Navigator server is requesting authentication
private const string LOGIN_PAGE = "login.form";

// this is an identifier in the HTML response that indicates the login failed
private const string INVALID_PASSWORD_ID = "INVALIDPASS";

private static log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

// just a unique identifier for this plugin
public string Name { get { return "AuthenticationSample"; } }

/*
* Authenticate()
* Check if authentication is necessary and attempt to login if so.
* return values:
* -return AuthenticationStatus.NONE if nothing was done or authentication not required
* -AuthenticationStatus.AUTHENTICATED if authentication was successful
* -AuthenticationStatus.AUTHENTICATION_FAILED if authentication was required but failed
* -AuthenticationStatus.SESSION_EXPIRED if a session expiration was detected
*
* Parameters:
* HttpWebRequest webRequest: the web request that was sent to the Navigator server
* HttpWebResponse webresponse: the response from the server, this could be null if the request threw an exception
* Exception requestException: the exception thrown by the webRequest, if any. This could be null.
* string responseStr: the response from the webRequest, saved into a string. This could be null or empty if there was an exception
* string userName: the username entered in the login form. If empty then this request occurred before the user was prompted.
* string password: the password entered in the login form. If empty then this request occurred before the user was prompted.
* string loginUrl: the Navigator URL used.
*
*/
public AuthenticationStatus Authenticate(HttpWebRequest webRequest, HttpWebResponse webresponse, Exception requestException, string responseStr, string userName, string password, string loginUrl)
{
AuthenticationStatus retVal = AuthenticationStatus.NONE;

// username/password have not been supplied yet, don't do anything
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
return retVal;

// check the response string for the HTML login form
if (webresponse != null && webresponse.ContentType == "text/html" &&
responseStr != null && responseStr.Contains(LOGIN_PAGE))
{
log.InfoFormat("Found login form entity {0} in webresponse {1}", LOGIN_PAGE, webresponse.ResponseUri.ToString());

// post the credentials to the login page:
string loginPageUrl = webRequest.RequestUri.GetLeftPart(UriPartial.Authority) + "/" + LOGIN_PAGE;
HttpWebRequest loginRequest = (HttpWebRequest)WebRequest.Create(loginPageUrl);
loginRequest.UserAgent = webRequest.UserAgent;
loginRequest.Method = "POST";

// hard coded the form elements for now:
string postData = "username=" + userName + "&password=" + password + "&login-form-type=pwd";
byte[] bytes = Encoding.ASCII.GetBytes(postData);
loginRequest.ContentLength = bytes.Length;

Stream oStreamOut = loginRequest.GetRequestStream();
oStreamOut.Write(bytes, 0, bytes.Length);
oStreamOut.Close();

loginRequest.KeepAlive = true;

// Use the same CookieContainer that was used in the webRequest. This means the same session is used
// in the login form post request, and any session tokens created as a result of the login will
// automatically be saved into the current session.
// If for whatever reason you need to use a separate session/cookieContainer, then you need to update
// the current session with the new cookie container after login by setting the public static:
// HttpRequestResponse.CookieContainer
loginRequest.CookieContainer = webRequest.CookieContainer;
HttpWebResponse loginResp = (HttpWebResponse)loginRequest.GetResponse();
StreamReader sr = new StreamReader(loginResp.GetResponseStream(), System.Text.Encoding.ASCII);
string tempstr = sr.ReadToEnd();
sr.Close();
loginResp.Close();

// check for bad password error:
// this is just intended as a sample
if (tempstr.Contains(INVALID_PASSWORD_ID))
{
log.InfoFormat("Authentication failed for user {0}, request: {1}\n response string {2}", userName, loginRequest.RequestUri, tempstr);
return AuthenticationStatus.AUTHENTICATION_FAILED;
}

log.InfoFormat("Authentication successful, response string:\n{0}", tempstr);
retVal = AuthenticationStatus.AUTHENTICATED;
}
return retVal;
}
}
}

Logout

If there are any session cleanup tasks that need to be performed on a logout call, the plugin can do this by checking the webRequest argument for the logout call in the request Uri. This is not required and is configuration dependent.

Debugging

To debug the code, it is easiest to start on a computer that has NMO installed. NMO is configured to load all the plugins in the Plugins directory in the NMO install directory, so copy the plugin’s debug dll and pdb file to the Plugins directory. To start debugging, set the plugin project’s debug setting to start an external program, either Word.exe, Excel.exe, etc., and then just press F5 to start debugging.

Deployment

The plugin dll has to be copied to the NMO installation directory, in the Plugins directory. While there is not a way to integrate a custom dll into the NMO installer, the installer does have a feature to check a predefined directory and copy any files there into the Plugins directory. To use this feature, create a directory called “Plugins” off of the directory that the ICNforOfficeInstall.exe file is located. During installation any files in this directory will be copied down to the NMO install directory.

[{"Product":{"code":"SSEUEX","label":"IBM Content Navigator"},"Business Unit":{"code":"BU053","label":"Cloud & Data Platform"},"Component":"ICN for MS Office with FileNet CM","Platform":[{"code":"PF033","label":"Windows"}],"Version":"2.0.3;3.0.0;3.0.1","Edition":"","Line of Business":{"code":"LOB18","label":"Miscellaneous LOB"}}]

Document Information

Modified date:
17 June 2018

UID

swg21979403