Some Thoughts On Handling 401 Unauthorized Errors With jQuery

Posted July 13, 2011 at 10:57 AM by Ben Nadel

Tags: ColdFusion, Javascript / DHTML

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

Jul 13, 2011 at 11:38 AM // reply »
1 Comments

Very elegant solution to a common and annoying AJAX problem. Thanks for sharing!


Jul 13, 2011 at 11:41 AM // reply »
11,238 Comments

@Martin,

My pleasure! I hope I am on the right track here.


Jul 13, 2011 at 11:56 AM // reply »
9 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


Jul 13, 2011 at 3:01 PM // reply »
11,238 Comments

@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.


Jul 13, 2011 at 3:24 PM // reply »
369 Comments

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.


Jul 13, 2011 at 8:37 PM // reply »
11,238 Comments

@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!


Jul 14, 2011 at 12:48 PM // reply »
1 Comments

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.


Jul 14, 2011 at 12:55 PM // reply »
11,238 Comments

@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.


Jul 18, 2011 at 5:17 AM // reply »
18 Comments

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


Jul 20, 2011 at 10:31 AM // reply »
11,238 Comments

@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.


MJ
Aug 17, 2011 at 3:55 PM // reply »
1 Comments

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.


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
May 14, 2013 at 6:57 PM
The UX Of Prototyping: Low-Fidelity Is The New High-Fidelity
@Phillip, I'm not sure I follow what you mean? Are you saying that you looked at the list of widgets provided by the jQuery UI and let that be your style guide? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools