DojoFAQ

A common scenario with web applications is accessing protected resources, which require authentication with the server in order to proceed. A common flow is as follows:

  1. User opens web site
  2. Web site presents authentication screen
  3. User enters credentials
  4. Web site presents protected information

Authentication workflows

This is simple enough to begin with, but what happens when the session times out? Or perhaps the application does not require authentication initially, but once the user initiates an action to access protected resources authentication is required? A common approach is to use redirects to an authentication page:

  1. User’s authentication session times out
  2. Any action on user’s part redirects to authentication screen
  3. After successful authentication, user is redirected to the results of the initial action

This model works well enough with applications architected around the full page request-response model, but becomes less pleasant with single-page applications that provide a persistent and responsive user interface. A solution that provides a better end-user experience is to mimic the general approach above without the actual page redirects. The authentication can be performed without causing a full page reload:

  1. User’s authentication session times out (assume client-side application code has no knowledge of session status)
  2. User initiates an action which is handled normally by the application code and makes an XHR request to the server
  3. The server detects that the session has timed out and instead of returning the requested resource it returns a 403 (Forbidden) status code
  4. Client-side application code has been configured to intercept all 403 responses and present an authentication dialog
  5. User provides credentials and re-authenticates
  6. Initial request is repeated; server returns appropriate resource; application flow continues as normal

Emulating redirects client-side

The challenge is, how can step #4 be implemented using dojo/request? By creating your own request provider you can set up a robust system that works across your application without having to update code in individual application components.

Let’s look at what we need to do in a little more detail. The goal is to provide an optimal end-user experience and also maintain a clean separation of concerns in the source code. The components that make up the application should not have to handle authentication. The interruption caused by the need to authenticate should happen transparently to the application. Fortunately, dojo/request allows us to do this. There are two main issues to address:

  1. Intercepting XHR calls to check if 403 handling needs to be performed: this can be achieved with a custom request provider
    • After step 2 completes, re-send the original XHR so the application flow can resume
  2. Creating a method and user interface to present an authentication dialog

Creating a custom request provider

A request provider is simply a module that implements the dojo/request API. In this case, we want the behavior provided by dojo/request/xhr so we don’t need to provide a complete implementation – we can extend dojo/request/xhr to inherit most functionality. The extra functionality we want to provide is intercepting responses with a status code of 403.

To provide an example of the simplest request provider, here is a request provider that defers entirely to dojo/request/xhr and adds no custom functionality:

define([
	'dojo/request/xhr',
	'dojo/request/util'
], function (request, requestUtil) {
	function requestProvider(url, options) {
		return request(url, options);
	}

	// Add the 'get', 'post', etc. convenience methods
	requestUtil.addCommonMethods(requestProvider);

	return requestProvider;
});

The module above can now be used in place of dojo/request. Let’s continue to customize this to add the required functionality:

define([
	'dojo/_base/lang',
	'dojo/request/xhr',
	'dojo/request/util',
	'dojo/Deferred'
], function (lang, request, requestUtil, Deferred) {
	function requestProvider(url, options) {
		// Create the Deferred that will manage the overall request
		// process
		// * Should resolve to the requested data
		// * Should only be resolved if the XHR response returns
		//   a non-error status
		// * If the XHR response returns a 403 status code, the
		//   authentication process should be asynchronously injected
		// * If authentication is cancelled by the user, the Deferred
		//   should be rejected
		var dfd = new Deferred();

		// While you normally return the 'promise' property of a
		// Deferred, we need to return a promise that has a 'response'
		// property on it.
		// The 'promise' property of a Deferred is frozen, so we cannot
		// add properties to it. In order to create a mutable promise, we
		// use 'lang.delegate' to create an object whose prototype is
		// linked to 'dfd.promise'.
		var dataPromise = lang.delegate(dfd.promise.then(function (response) {
			return response.data || response.text;
		}));

		// 'dfd' will resolve to the 'response' property of the XHR,
		// which is what we want on dataPromise's 'response' property
		dataPromise.response = dfd.promise;

		// This function uses dojo/request/xhr to perform the actual XHR
		function send() {
			// Perform the xhr request
			var xhrPromise = request(url, options);

			// Listen for the response to the request
			xhrPromise.response.then(function (response) {
				// Resolve the surrogate deferred when the original
				// request was successful
				dfd.resolve(response);
			}, function (error) {
				// Otherwise check to see whether the request failed due
				// to a 403 from the server
				if (error.response.status === 403) {
					// Perform some reauthentication in the UI
					reauthenticate().then(function () {
						// Re-run the original request
						send();
					}, function (error) {
						// User failed to reauthenticate successfully,
						// permanent failure
						dfd.reject(error);
					});
				}
				else {
					dfd.reject(error);
				}
			});
		}

		send();

		return dataPromise;
	}

	// Add the 'get', 'post', etc. convenience methods
	requestUtil.addCommonMethods(requestProvider);

	return requestProvider;
});

You could of course load this module instead of dojo/request in your application code, but there are other solutions available that may be more convenient, such as configuring a default request provider or remapping AMD modules.

Configuring a default request provider

If you have created a request provider that you want to be used by modules throughout your application that load dojo/request, you can use the requestProvider property in your Dojo config. For example, if you created a request provider in app/custom/request.js:

var dojoConfig = {
	async: true,
	requestProvider: 'app/custom/request'
};

This would result in all modules that load dojo/request receiving the module defined in app/custom/request.js. Some points to keep in mind:

  • Modules that explicitly load dojo/request/xhr will be unaffected by this
  • dojo/request/default.js includes logic to load the appropriate default provider for web clients (dojo/request/xhr) and Node.js (dojo/request/node). If this is relevant to your application you will have to implement this logic in your custom provider.

Dojo’s request modules also include dojo/request/registry, which allows you to specify which request provider module to use based on request criteria.

Remapping AMD modules

If you want to restrict use of your custom provider to a subset of your application’s modules, or if you have modules that directly load dojo/request/xhr, you can use Dojo’s map configuration option to remap modules.

var dojoConfig = {
	map: {
		'app/custom': {
			'dojo/request': 'app/custom/request',
			'dojo/request/xhr': 'app/custom/request'
		}
	}
};

This configuration would result in all modules whose path begins with “app/custom” receiving the app/custom/request module instead of dojo/request or dojo/request/xhr. You can read more about using map under the “Using portable modules” section of the Advanced AMD Usage tutorial.

Creating an authentication module

// The reauthenticate function stores its own state in a closure
var reauthenticate = (function () {
	var dfd;

	// Precreate some custom widget for the reauthentication form
	// This is where you would supply your own implementation
	var form = new AuthenticationForm();

	// When the form is submitted...
	form.on('submit', function (data) {
		// Hide the form
		form.hide();

		// Try to authenticate using the data from the form
		request.post('authenticate', {
			handleAs: 'json',
			data: data
		}).then(function (response) {
			// If the authentication was successful, resolve the deferred
			if (response.success) {
				dfd.resolve();
				dfd = null;
			}
			// Otherwise clear the form and display the authentication
			// error from the server
			else {
				form.clear();
				form.set('error', response.message);
				form.show();
			}
		});
	});

	// If someone cancels the authentication form then they have given up
	// reauthenticating and the application needs to do something else
	form.on('cancel', function () {
		dfd.reject();
		dfd = null;
		form.hide();
	});

	return function () {
		// If we are already in the reauthentication workflow, simply
		// return the same promise for all future calls
		if (dfd) {
			return dfd.promise;
		}

		// Otherwise create a new deferred for the form and show it
		dfd = new Deferred();
		form.show();

		return dfd.promise;
	};
})();

To clarify, the authentication module code above requires you to provide your own implementation of an AuthenticationForm module that provides the methods being called above, as well as an authentication endpoint for the XHR on line 15.

Conclusion

With a custom request provider in place to respond to specific responses from the server and a module to provide the user interface for authentication you can maintain the smooth user experience in your single-page applications even when the user must authenticate (or re-authenticate). The example code allows for infinite retries with no delay – any security precautions for a production environment should be deployed on the server.

Learning more

SitePen covers advanced Ajax usage and much more in our Dojo workshops offered throughout the US, Canada, and Europe, or at your location. We also provide expert JavaScript and Dojo support and development services. Contact us for a free 30 minute consultation to discuss how we can help.