Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ellen Kaspern

Some Thoughts On Handling 401 Unauthorized Errors With jQuery

By Ben Nadel on

As I continue to make the journey from the "traditional" request-response web application design to thick-client, AJAX-driven, realtime applications, I am always finding new problems to solve. Yesterday, on a project, we started seeing an issue where a user would have two tabs open for the same app; then, they would explicitly log-out of one tab which would cause all of the AJAX requests in the other tab to break (they were being diverted to the login page). To fix this in the quickest, easiest way possible, we set up a global AJAX handler that would listen for 401 Status Codes and redirect the user to the login page.


 
 
 

 
  
 
 
 

The first part of our problem was that our login page was not returning a 401 status code. This didn't matter for AJAX requests that were expecting JSON responses (as they would fail anyway); the problem really become obvious with AJAX requests that were expecting HTML responses. To these requests, the Login HTML was just as good as the Modal Window HTML.

By adding a 401 status code to the login page, at least we could start to get all AJAX requests to fail across the board (both those expecting JSON and HTML return data).

Once we got the AJAX requests to start failing, we had to figure out a way to forward the user to the login page once an asynchronous 401 status code was returned. This particular project has a whole mess of AJAX functionality; so, we didn't want to go through and retrofit each request/response interaction. Instead, we configured a global AJAX handler that listened specifically for the 401 status code; and, if it was triggered, it would use JavaScript to forward the user to the Login page.

To demonstrate this, I put together an extremely simple application that maintains session and has one AJAX request. If you watch the video above, you can see this in action; but, let's step through the code.

I'm gonna start with the main index page as this is really the only thing worth while - the other pages are just there to support logged-in/logged-out status.

Index.cfm - The "Application" Interface

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Handling 401 Errors With jQuery</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Handling 401 Errors With jQuery
  • </h1>
  •  
  • <h2>
  • Messages From The Server:
  • </h2>
  •  
  • <ul class="messages">
  • <!-- To be populated dynamically. -->
  • </ul>
  •  
  •  
  • <!-- Include the jQuery library. -->
  • <script type="text/javascript" src="../jquery-1.6.1.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // I get the next message from the server.
  • function getNextMessage(){
  •  
  • // Get the message from the server.
  • var request = $.ajax({
  • type: "get",
  • url: "./message.cfm",
  • dataType: "json"
  • });
  •  
  • // When the message comes back successfully, add it to
  • // the message queue.
  • request.done(
  • function( response ){
  •  
  • $( "ul.messages" ).append(
  • "<li>" + response + "</li>"
  • );
  •  
  • }
  • );
  •  
  • // When the message comes back as an error, simply alert
  • // that the error has occurred.
  • request.fail(
  • function(){
  •  
  • alert( "Oops, something went wrong!" );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // Set up an interval for getting the new messages.
  • setInterval( getNextMessage, (5 * 1000) );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Set up a global AJAX error handler to handle the 401
  • // unauthorized responses. If a 401 status code comes back,
  • // the user is no longer logged-into the system and can not
  • // use it properly.
  • $.ajaxSetup({
  • statusCode: {
  • 401: function(){
  •  
  • // Redirec the to the login page.
  • location.href = "./login.cfm";
  •  
  • }
  • }
  • });
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we have one AJAX method - getNextMessage() - that runs on a 5-second interval, requesting messages from the server. Typically, these message requests come back with a 200 OK response and the page is updated accordingly. If, however, the user becomes logged-out, these AJAX requests will start coming back with a 401 Unauthorized response.

To handle this 401 status code, we are using the $.ajaxSetup() function to define a default 401 status code handler. This handler will be merged into all AJAX requests within the entire application (assuming they don't override it) and will therefore be triggered when any AJAX request comes back with a 401 status code.

NOTE: Status Code callbacks and "error" callbacks co-exist. This means that a 401 status code callback will not prevent an error callback from being used - and vice-versa. Should a 401 error come back, both the error handler and the 401 callback will be invoked.

I don't know if this is the right way to do things; this is just the first idea that we came up with when we started to see this multi-tab issue arise. And, it seems to have solved the problem reasonably well.

Now that you see how the main, AJAX-driven part of the application is working, let's quickly run through the support files.

Application.cfc - The ColdFusion Application Framework

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application settings. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 10, 0 ) />
  • <cfset this.sessionManagement = true />
  • <cfset this.sessionTimeout = createTimeSpan( 0, 0, 10, 0 ) />
  •  
  •  
  • <cffunction
  • name="onSessionStart"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I initialize the session.">
  •  
  • <!---
  • Keep a simple flag for the user being logged in / out.
  • This demo just requires this to be toggled, not really
  • functional.
  • --->
  • <cfset session.isLoggedIn = false />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I execute the incoming request.">
  •  
  • <!--- Define the arguments. --->
  • <cfargument
  • name="script"
  • type="string"
  • required="true"
  • hint="I am the script requested by the user."
  • />
  •  
  • <!---
  • Check to see if the user is logged-in. If they are, they
  • can execute any script they want; otherwise, they have to
  • log-in.
  • --->
  • <cfif session.isLoggedIn>
  •  
  • <!--- Execute the requested script. --->
  • <cfinclude template="#arguments.script#" />
  •  
  • <cfelse>
  •  
  • <!---
  • Whoa buddy! You need to be logged-in. Include the
  • login page regardless of what script was requested.
  • --->
  • <cfinclude template="./login.cfm" />
  •  
  • </cfif>
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

The Application.cfc just defines the application settings and manages the incoming requests. As you can see above, the user's session has a simple logged-in flag. If the user is logged-in, they can request any script; if they are logged-out, all requests ultimately execute the login page.

The login page then provides hooks to both log into and out of the system:

Login.cfm

  •  
  • <!---
  • Check to see if the login has been executed (for this demo, we
  • aren't actually using any credentials - we are just toggling the
  • logged-in status when requested.
  • --->
  • <cfif structKeyExists( url, "login" )>
  •  
  • <!--- Toggle logged-in status. --->
  • <cfset session.isLoggedIn = true />
  •  
  • </cfif>
  •  
  •  
  • <!--- Check to see if the logout has been executed. --->
  • <cfif structKeyExists( url, "logout" )>
  •  
  • <!--- Toggle logged-in status. --->
  • <cfset session.isLoggedIn = false />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • The user had ended up on the login page. However, they may have
  • accessed it directly; or, they may have been forwarded here by
  • the security logic. If they were forwarded here, we want to send
  • back an unauthorized error.
  •  
  • NOTE: We could just always send back a 401 status code; but, I
  • thought this check might be a bit more elegant.
  • --->
  • <cfif (expandPath( cgi.script_name ) neq getCurrentTemplatePath())>
  •  
  • <!---
  • The requested script is NOT the current script - the user
  • has been forced to this page for authorization.
  • --->
  • <cfheader
  • statuscode="401"
  • statustext="Unauthorized"
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- Reset the output buffer. --->
  • <cfcontent type="text/html" />
  •  
  • <cfoutput>
  •  
  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Please Login</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Please Login
  • </h1>
  •  
  • <p>
  • Logged-in Status: #session.isLoggedIn#
  • </p>
  •  
  • <p>
  • <a href="./index.cfm">Goto homepage</a>.
  • </p>
  •  
  • <p>
  • <a href="./login.cfm?login=true">Login</a>
  • &nbsp;|&nbsp;
  • <a href="./login.cfm?logout=true">Logout</a>.
  • </p>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

As you can see, this page simply provides a way to toggle the logged-in status of the user for testing purposes. One of the most critical aspects of this page, however, is the fact that the login page returns a 401 Unauthorized status code. It's this status code that allows the AJAX requests to invoke the proper success and error callbacks.

I could probably have returned the 401 status code all the time; but, for a little added elegance, I am only returning the 401 status code if the user requests a page that is not the login page. So, if they access the login page directly, there is no authorization required.

And, or course, we had our message AJAX request that simply returned a string:

Message.cfm - Our AJAX Target Page

  • <!--- Define a message. --->
  • <cfset message = "It is now #timeFormat( now(), 'hh:mm:ss TT' )#" />
  •  
  • <!--- Return the JSON. --->
  • <cfcontent
  • type="application/json"
  • variable="#toBinary( toBase64( serializeJSON( message ) ) )#"
  • />

Pretty much nothing going on there.

Again, I don't know if this is the best way to handle authorization in an AJAX-driven application; but, by making judicious use of the 401 status code and by defining a global 401 AJAX response handler, we were able to fix our problem in just a matter of minutes. Every day, I find a new reason to love jQuery even more!




Reader Comments

We use a common error "container" for putting up ajax errors. While you are handling 401 errors and redirecting, this snippet will catch any ajax error

$("#errorbox").ajaxError(function(event, xhr, settings, err) {
$("#errorbox").text(xhr.statusText);
});

When architecting your ajax stub pages, if you use cfheader (like you do) in your try/catch statements and use custom status codes and status text instead of returnig the error in the data you can catch all (well most) of your ajax errors by putting a case statement in your ajaxError function to catch each "custom" error. Put the ajaxError code in a commom file and make sure to include the error container on all your pages and you have a fairly easy to implement application wide ajax error handler, kinda like onError() for application.cfc

Reply to this Comment

@MikeG,

I like it! I have to admit that my error handling is never all that pretty. I see some beautiful forms where the error handling is so tightly integrated. I tend to use an alert() box or an unordered list.

I think using this kind of error handling in conjunction with some different pre-handler filtering:

http://www.bennadel.com/blog/2132-Using-ajaxPrefilter-To-Configure-AJAX-Requests-In-jQuery-1-5.htm

... would be a sweet combination. That way, I could use things in the 40x range for validation AND, still use 401 in a more global way.

Reply to this Comment

I know you're supposed to do error handling not only using javascript, but also for browsers where the javascript is turned off, but I have to admit, I LOVE some of the beautiful jQuery error handling. I have come across some awesome jQuery error handling stuff in my time. On the other hand, for the browsers that have javascript turned off, I have seen some pretty amazing css stuff, too, but I have to admit, I don't really know how to use css all that well. It's a huge mystery to me, and I am always encountering things with css that give me huge headaches and are extremely picky and finicky. Sometimes, there are just some things I CAN NOT get to work using css, and I have to resort to html tricks instead. Anyway, sorry about my little rant there. Just thought I'd throw my .02 in.

Reply to this Comment

@Anna,

For this particular application (where this problem popped up), JavaScript is a requirement. With JavaScript turned off, you wouldn't even be able to use the core features of the app.

That said, there is definitely some beautiful CSS out there, that I am jealous of!

Reply to this Comment

Ben, Same use-case may occur when session is expired in fully ajax base app and if this special case is not considered when building it by setting proper redirection on client side or sending special response code/flag from server.

Reply to this Comment

@Sachin,

Ah, very good point! I hadn't even thought of that. In this particular app (the one causing the original problem), we do have a "heart beat" AJAX request that pings the server like every 15 minutes just to make sure the session doesn't die. But, you're absolutely right - without that, the session expiration would cause the exact same problem.

Reply to this Comment

Hopefully someone can provide more insight, but I use something similar to the above, but believe it is in violation of the standard. (However, I've never seen any ill consequences of it.)

When sending 401, "The response MUST include a WWW-Authenticate header field (section 14.47) containing a challenge applicable to the requested resource."

requirement mentioned here:

http://tools.ietf.org/html/rfc2616#section-10.4.2

detailed here:

http://tools.ietf.org/html/rfc2617#section-3.2.1

Reply to this Comment

@Grumpy,

Hmm, very interesting. I had not heard that before - but that doesn't mean anything - I'm not exactly well versed in the HTTP standard. In this case, we are definitely not using Basic OR Digest authentication. As such, I wouldn't know what type of value to put in the WWW-Authenticate header. I'll see if I can find out some more information on this.

Reply to this Comment

How do I capture the url that returned 401. I am making a cross domain calls different sites using proxy and I need to capture the url in the status code 401 function so that I can get the authentication token using OpenID for that site.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.