Digest Authentication with Appcelerator Titanium HTTPClient

Digest Authentication with Appcelerator Titanium HTTPClient

I’ve been working with web technologies for over a decade and never had to touch Digest Access Authentication.  All of the services I had worked with had other solutions for authentication.  A few months back that all changed.  I was finishing up a mobile project, to be used for internal purposes for the client, when suddenly the API requirements changed.  They had implemented Digest Access Authentication with MD5.  Uh-oh.

What is Digest Authentication?

Let’s start small; I certainly had to.  Digest Access Authentication is a method for validating a user and granting them some access over HTTP.  It involves making a request, being presented with a challenge, answering that challenge with another request, and finally getting the resource originally requested.  It works like this:

  1. Client sends request to the server
  2. Server responds with a nonce (number to be used once) with a status of 401
  3. Client makes another request to the server providing identification information
  4. Server evaluates whether the user is valid and if they are who they say are
  5. Server responds with desired resource

How do we do this in Titanium?

The idea is to send out requests like normal, check for a 401 status code, and respond to the presented challenge if applicable.  That is simple enough:


//Create the HTTPClient
xhr = Titanium.Network.createHTTPClient();

xhr.onload = function(){
	//Process the response
}

xhr.onerror = function(){
	//Process an error
	if(this.status == 401){
		//Respond to challenge
	}
}

When I was originally tackling this problem I found a very helpful example on github by rollsroyc3: https://gist.github.com/rollsroyc3/6869880  The majority of the following code is from that example, but I made a few changes.

Before adding any code to actually handle the challenge, let’s take a step back.  Assuming you have multiple HTTPClients, like I did, every HTTPClient would need to be rewritten.  Instead, let’s encourage code reuse and turn this into a commonJS lib that wraps the HTTPClient.  Then we can have one HTTPClient that does Digest Authentication when it encounters a 401 status code and acts normally with all others:

(I use underscore.js a bit here, and you should too)


var timeout = 2000;

 
 /**
  * The authentication magic.
  *
  * Breaks down a 401 request from a digest authentication endpoint and generates a response
  * 
  * @param  {Response Headers} h : The response headers
  * @return {Request Headers} : The request headers to match the response
  */
function parseAuthenticationResponse(h) {
	var auth = {
		headers : {}
	};
	var scre = /^\w+/;
	var scheme = scre.exec(h);
	auth.scheme = scheme[0];
 
	var nvre = /(\w+)=['"]([^'"]+)['"]/g;
	var pairs = h.match(nvre);
 
	var vre = /(\w+)=['"]([^'"]+)['"]/;
	var i = 0;
	for (; i < pairs.length; i++) {
		var v = vre.exec(pairs[i]);
		if (v) {
			auth.headers[v[1]] = v[2];
		}
	}
	return auth;
}

/**
 * Generates a unique token for a CNONCE value using the device ID and current time
 * @return {String} : Unique token for a CNONCE value
 */
function generateUniqueCNONCE(){
	return new Date().getTime() + Ti.Platform.id;
}
 

/**
 * Performs the secondary request with our response to the challenge
 * @param  {String}   requestType : HTTP verb
 * @param  {String}   url         : The location of the desired resource
 * @param  {Object}   postData    : Any data that needs to be sent with the request
 * @param  {String}   header      : The Authorization Header
 * @param  {Function} onload      : Callback if successful
 * @param  {Function} onerror     : Callback if failure
 * @return {void}             
 */
exports.httpClient = function(requestType, url, postData, header, onload, onerror) {
	//Create the client
	var xhr = Titanium.Network.createHTTPClient();

	//We've already tried to authorize at this point, send the appropriate callbacks
	xhr.onload = function() {
		if (_.isFunction(onload)){
			onload((this.responseText ? JSON.parse(this.responseText) : null), xhr);
		}
	};
 
	xhr.onerror = function(e) {
		if(_.isFunction(onerror)){
			onerror(xhr, e);
		}
	};

	xhr.setTimeout(timeout);

	//Perform the request
	xhr.open(requestType, url);
 
 	//Set the header
	header && xhr.setRequestHeader("Authorization", header);
 
	if ((requestType == 'POST' || requestType == 'PUT' || requestType == 'DELETE') && postData) {
		//Set the content type
		xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
		//Stringify the post data, to ensure it's not url encoded
		xhr.send(JSON.stringify(postData));
	} else {
		xhr.send();
	}
};

/**
 * Performs the initial request
 * @param  {String}   requestType : HTTP verb
 * @param  {String}   url         : The location of the desired resource
 * @param  {Object}   postData    : Any data that needs to be sent with the request
 * @param  {Function} onload      : Callback if successful
 * @param  {Function} onerror     : Callback if failure
 * @param  {Object}   user        : Object containing username nad password for auth
 * @return {void}             
 */
exports.httpClientDigest = function(requestType, url, postData, onload, onerror, user) {
	//Create the client
	var xhr = Titanium.Network.createHTTPClient();

	//If all goes well we don't need to do anything!
	xhr.onload = function() {
		if(_.isFunction(onload)){
			onload((this.responseText ? JSON.parse(this.responseText) : null) || this.responseText, xhr);
		}
	};
 
 	//If there's an error, we need to check for an authentication challenge
	xhr.onerror = function(e){
		if (this.status == 401) {
      		//401 error, time to authenticate
			var headers = this.getResponseHeaders();
			var tokensObj = parseAuthenticationResponse(headers['Www-Authenticate']);
 
 			//Build the headers
			tokensObj.headers.cnonce = generateUniqueCNONCE();
			tokensObj.headers.nc = '00000001';
			tokensObj.headers.algorithm = 'MD5';
			tokensObj.headers.method = requestType;
			tokensObj.headers.domain = url;
 
			var HA1 = Ti.Utils.md5HexDigest(user.username + ':' + tokensObj.headers.realm + ':' + user.password);
			var HA2 = Ti.Utils.md5HexDigest(tokensObj.headers.method + ':' + tokensObj.headers.domain);
			var authResponse = Ti.Utils.md5HexDigest(HA1 + ':' + tokensObj.headers.nonce + ':' + tokensObj.headers.nc + ':' + tokensObj.headers.cnonce + ':' + tokensObj.headers.qop + ':' + HA2);
			var responseContentHeader = 'Digest username="' + user.username + '"' + ', realm="' + tokensObj.headers.realm + '"' + ', nonce="' + tokensObj.headers.nonce + '"' + ', uri="' + tokensObj.headers.domain + '"' + ', algorithm="' + tokensObj.headers.algorithm + '"' + ', response="' + authResponse + '"' + ', qop="' + tokensObj.headers.qop + '"' + ', nc=' + tokensObj.headers.nc + ', cnonce="' + tokensObj.headers.cnonce + '"';
      
      		//Respond to the authentication and request the resource again
			exports.httpClient(requestType, url, postData, responseContentHeader, onload, onerror);
		}
		else{
			//Some other error, let the callback handle it
			if(_.isFunction(onerror)){
				onerror(xhr, e);
			}
		}
	};

	xhr.setTimeout = timeout;

	//Perform the request
	xhr.open(requestType, url);
	
 
 	//Evaluate whether there is data to send
	if ((requestType == 'POST' || requestType == 'PUT' || requestType == 'DELETE') && postData) {
		//Set the content type
		xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
		//Stringify the post data, to ensure it's not url encoded
		xhr.send(JSON.stringify(postData));
	}
	else {
		xhr.send();
	}
};

We can then require this in and use it:


var httpDigest = require('httpDigest');

//Data to send
var data = {
	test: 'string'
};

//Connect through HTTP Client with Digest
httpDigest.httpClientDigest('POST', address, data, function(response, client){
	//Do something on success
},function(client, e){
	//Do something on failure
}, {username: 'username', password: 'password'});

Improvements

This is just one example of how it can be accomplished.  Your needs may be different.  There are many more options available to the HTTPClient that are not exposed (like headers).  This was a quick and dirty solution to a last minute problem.

If I needed the library today for a new project I would modify it to make use of Backbone events.  Imagine different components being notified when a new request has been made, when that request has been met with Digest Access Authentication, and when the response is finally successful!  Evented networks are both a blessing and a curse.  Allowing various controllers to listen for updates to data is amazing, but if you’re not careful with cleanup then you’re begging for memory leaks.