Earlier today a user on cf-talk asked about handling session time outs in an Ajax based application. I'm happy he asked this because I'm been worrying about this myself for a while now. One of my applications, Lighthouse Pro, makes heavy use of Ajax. The main issue display is entirely Ajax driven. If your session times out though you get... nothing. That sucks. So I used this as an excuse to get off my butt and fix it once and for all. I also whipped up a quick simple example that I'll use for the blog entry. I'd love to get people's opinions on it and start a discussion about how others have solved this issue as well. (I'll also say that there will be a follow up later talking about handling ColdFusion errors in general.)
To begin, let's create an incredibly simple application. First, our Application.cfc with a super simple security system.
public boolean function onRequestStart(string req) {
if(structKeyExists(form, "login")) {
session.loggedin = true;
} if(!structKeyExists(session, "loggedin") && listlast(arguments.req,"/") != "login.cfm") {
location(url="login.cfm");
}
return true;
} }
component {
this.name="ajaxtimeout";
this.sessionManagement="true";
this.sessiontimeout=createTimeSpan(0,0,0,31);
As you can see, we use the onRequestStart method to look for a session variable. If it doesn't exist, we push the user to the login page. If we see the login key in the form scope, we mark the user as logged in. (This is normally where you would actually check the username and password.) Now let's look at our Ajax "application."
<html> <head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
$(document).ready(function() { $("#testLink").click(function(e) {
$("#showResult").load("rand.cfm");
e.preventDefault();
});
})
</script>
</head> <body> Click the link to perform an Ajax request to load a random number: <a href="" id="testLink">Click Me, Baby</a> <p> <div id="showResult"></div> </body>
</html>
Woot. That's no GMail for sure. The so-called application has one link. When you click on it I'm going to load the contents of rand.cfm into the div. rand.cfm simply outputs a random number so I won't bother showing the code for that. So you may have noticed that my session timeout was extremely small. If you run this code as is, login (you can enter anything or nothing), and then let the app site for a bit over half a minute, you will see that clicking the link returns the login form to the user. In a more complex Ajax app where JSON was requested, the login form wouldn't be valid JSON so nothing at all would happen. (As with Lighthouse Pro.) Here is an example:
So, let's talk about how we can handle this. Currently our security logic is that if you are not logged in, we push you to a new CFM. That works fine for normal requests, but for an Ajax request we should handle it differently. We could simply output a string, like "SessionTimeout", but then we would need to update our jQuery code to look for that particular string. Instead, why not simply throw an error that we can listen for? Let's look at an example of that first:
if(!structKeyExists(session, "loggedin") && listlast(arguments.req,"/") != "login.cfm") {
//for ajax requests, throw an error
var reqData = getHTTPRequestData();
if(structKeyExists(reqData.headers,"X-Requested-With") && reqData.headers["X-Requested-With"] eq "XMLHttpRequest") throw(message="SessionTimeout");
else location(url="login.cfm");
}
return true;
}
public boolean function onRequestStart(string req) {
if(structKeyExists(form, "login")) {
session.loggedin = true;
}
I've modified onRequestStart to look for a Ajax request. This is done by looking at the HTTP Request Data. It isn't perfect but should work well for 99.99% of your clients. If the request is an Ajax one, I'm throwing an exception with a particular message. Otherwise we simply do the default location.
Luckily, jQuery provides an excellent way to handle errors across all Ajax requests in a page. The ajaxSetup method can be used to handle errors. Mahesh Chari has a great blog post on the topic: How to handle ajax errors using jQuery ? I used his example, modified, to look for a particular type of error we are going to work with in a bit.
$.ajaxSetup({
error:function(x,e){
if(x.status == 500 && x.statusText == "SessionTimeout") {
alert("Your session has timed out.");
location.href = 'login.cfm';
}
}
}); $("#testLink").click(function(e) {
$("#showResult").load("rand.cfm");
e.preventDefault();
});
})
</script>
<script>
$(document).ready(function() {
I love this code for a few reasons. First, notice that my original code is not changed. It's logic is the exact same. Instead, I added an error handler at the page level. It then can worry about handling errors whereas my other function can stay simple. How you handle this error is obviously up to you, but I decided to use the alert function to let the user know about the issue and then I push them to the login. Obviously it could be done a bit snazzier.
Woot! Everything is perfect. Almost... You may be wondering - what happens if you have an error handler for your site? (Sorry - I said "if", everyone has an error handler for their site.) Well, that makes things... interesting. Consider the following onError:
public void function onError(exception,eventname) {
writelog(file='application', text='my onerror ran');
}
This is a fairly simple, and useless, error handler. Normally you would want to tell the user something, but for now, it at least hides the default ColdFusion errors from returning to the screen. But now we are intentionally throwing an error - so what do we do? We end up then needing to actually rethrow the error. CFScript doesn't support rethrow, so it is a bit more wordier in the script version, but here is what I used:
public void function onError(exception,eventname) {
if(arguments.exception.rootcause.message == "SessionTimeout") throw(message=arguments.exception.rootcause.message);
writelog(file='application', text='my onerror ran: #serializejson(arguments.exception.rootcause.message)#');
}
Basically - look at the exception - and if you see that it was my Ajax-specific timeout, recreate it and let it leak out to the response.
Comments? As a quick note - you may wonder - what about normal ColdFusion errors? How would you handle that in an Ajax-based application. I'm going to talk about that next.
Archived Comments
Interesting approach Ray. Here's what I've done in the past:
* Log a timestamp of the last 'hit' by a given user in their session
* Create a simple facade that looks at the time difference between the last hit and the current time
* Create a ajax proxy to query that session facade
* Make sure the proxy request does not update the 'last hit' - otherwise your session will never time out
* Run a call to the session facade (via the ajax proxy) on a set interval
* Evaluate the inactivity period, if in a 'warning' period (say between 15-19 minutes) display a modal window warning the user.
* If the user chooses to extend the session, ping the session facade which will update the 'last hit' variable.
* If the inactivity is beyond the session timeout (I usually go with >= 19 minutes display a modal (non-closable) window telling them the session has timed out
(read more: http://cfsilence.com/blog/c...
Only thing with your method is that you'd have to modify it a bit to work with Flex (which has the same potential problem really).
In addition to handling timeout (or in lieu of) why not just make it so that the session cannot time out (unless server restarts). It is easy to do. In the old days it was a javascript timer that opened a pop-up window at coord 5000,5000 that had cfid/cftoken in the location url. The pop-up window closed itself. The new window request would "restart" the session timout timer. Other than freaking users out when a browser would show up and then fall of the task bar it worked well for most folks. When ajax became more mainstream and pop-up blockers more prevalent, the pop-up changed to an ajax call, with session.urltoken added to the url. This can be configured to be totally silent, or can generate a confirm dialog ("Hey, you! Are you done reading the page or do you need more time?") requiring user interaction.
@MikeG - that has the potential to be really, really, really bad. First of all - session timeout, among other things, protects dumb users. Imagine you left your Amazon account logged in and went to lunch. The girl in the cube next door decides she wants to buy a few hundred pairs of shoes and you're stuck with the bill. Secondly, sessions timeout to keep the servers memory at a healthy, manageable level. Imagine the size of your session scope after 30+ days of non expiring sessions....
@MikeG: The whole point of a session timeout is to help secure your application. If you really wanted no time out, I'd just use a super high timeout value. But I wouldn't recommend it.
@Todd: Why would I have an issue with Flex? In Flex you can also use fault handlers for service calls. Could I not then just use them?
I am going to have to respectfully disagree with both of you. First, @Todd, my post is essentially what your post said, with an option to either maintain the session silently or to ask the user if they want to extend it - you only ask them to extend it - which I would do for say a banking app. And when is the last time a user spent 30 days one of your web apps? As soon as they leave, your\my routine is no longer running and the session will timeout.
@Ray - the session timeout is not a security feature, it is a way to keep ram cleaned out..A logout routine that kills the session is a security feature. Relying on a session timeout for security is <ahem> lazy - sorry.
My job as a developer is to make the user experience super simple - let them focus on the task at hand - A user should not be expected to keep up with low level tasks like keeping their session alive. So, instead, you create a very small session timeout 3-5 minutes; and keep it alive --while they are on your site--. People get phone calls, called out into the front yard to deal with skinned knees, get interrupted by twitter or people wandering into their cube - and sometimes go to lunch. Their short attention span should not cause them to have to start the task over. Yes, it does have the potential to be bad, but so do asteroids hitting the earth. How likely is it going to happen? What is the risk? When you design your app you have to determine what is OK, what is not. In the case of LHP, keeping a session alive for a user all day is not a big deal - and that user will appreciate not having to log in every time they want to work a bug. For an ecomm app, it could be a bad idea. Sometimes we geeks need to step back and take a deep breath and remember that people use our apps, and there is no way we can protect them all; nor should we. Remember, nothing is foolproof to a sufficiently talented fool.
Woot! An argument. :>
First off - no one is saying to rely _only_ on session timeout. It is a part of a whole package of things you can do. Be honest - user's don't logout. Some do - most won't. So having a basic "if you are idle for N you are gone" is a VERY good backup. How do you differentiate between a user going to lunch and a user who was at a coffee shop going home? You can't.
(I said "First Off", but I think that's it)
(No, I lie, I've got more.)
Ecom: Many ecommerce sites will store your cart in a permanent scope (like client). So while you may log out, your cart still has items. The important stuff like your name, CC info, etc, is expired safely, but not your items.
I agree, users don't log out, but when a user is done with your site they make like a tree..so your session will time out naturally, but yours will do it in what, 20 minutes?, mine will do it in 3-5..
Shame on the user at the coffe shop going home..but, be honest, how many users at the coffe shop go home without at least closing thier browser. The days of internet cafe's with thier own 'puters are almost over; folks have ipads, netbooks, laptops and smart phones. but enough already; there is enough fodder here for folks to think about when planning an application.
About your post.. It is a good way to handle session timeouts for ajax calls where the whole page is a single ajax call. What would you do if your ajaxy stuff is in a widget, or dynamically changing a section of a form based on user input - and the user got interrupted. I think adding ajax into a website means you rethink how you "traditionally" managed a web app.
We will have to agree to disagree on point 1 - so let's move to point 2.
How is my solution only appropriate for a 'whole' page Ajax app? My demo is specifically _not_ that. It has content - and part of the page is loaded via Ajax.
well shoot, you got me in devil's advocate mode now...
BUT as i read over everything I realize my complaint is not how you catch the error, which is good, it is how you handle it..which is not the crux of the post, so I will just shut up now..
Thank you for making this lively. ;)
I still think you should keep the session alive for the user ;)
I use my dodgy User Love CFC - http://userlove.riaforge.org/ so the users are prompted to re-login again but then it continues on with whatever request they made.
And i just noticed i still have an open issue on the copy on RIAforge ><
oops, I should qualify that - in using User Love, I haven't had any issues with it with AJAX *yet*, but 99% of the calls I make with it are updating an area that actually has room to display a login box.
I actually covered this awhile ago as well, and I don't use jQuery to do it but ColdFusion instead. Although my blog post shows how to do it per ajax call, you can certainly use parts of what Ray has done with mine to achieve the result people are looking at doing.
I also use ExtJS to provide an event system if people wish to explore that further.
http://www.andyscott.id.au/...
Hey Ray,
I noticed this doesn't work unless "Enable Robust Exception Information " is check in the coldfusion admin. Is this normal to have this option checked for production servers (still assuming you are still hiding this extra info with a custom error)?
As far as I know that should have zero impact on this code. I'll give this a test to confirm.
It seems like it should without this option checked, but I tested this across Firefox, IE, and Chrome, and I can't duplicate the intended response unless that option is checked in the coldfusion admin.
You are absolutely correct! And yes - you do not want robust exception info turned on production. Thank you for finding this. I will have to correct this asap.
Initial discovery: The main issue is that statusText is now a vague message, not a specific one. So we have a few options. I'm digging into them now so I can hopefully recommend the best one.
What about adding your own custom header Ray?
Ok guys. So I have a solution which I think is decent. I'm not 100% convinced it is the best. My plan is to write it up as an update to this entry and edit the end of this entry to link to it. (Since not everyone reads comments.)
Here is the solution I used.
Within my Application.cfc, when a session timeout error occurs, I add a new header to the response. Unfortunately you can't use cfheader in cfscript yet so I have to use an include, which is kind of silly. But anyway, I do:
public void function onError(exception,eventname) {
if(arguments.exception.rootcause.message == "SessionTimeout") {
include "header.cfm";
throw(message=arguments.exception.rootcause.message);
}
writelog(file='application', text='my onerror ran: #serializejson(arguments.exception.rootcause.message)#');
}
My header.cfm is just:
<cfheader name="SessionTimeout" value="1">
Then I modified my JS to check for the header. I'd never done that before in JS but it was pretty trivial:
var sessionTimeout = x.getResponseHeader("SessionTimeout");
if(sessionTimeout == 1) {
alert("Your session has timed out.");
location.href = 'login.cfm';
}
I'll be posting this as a full entry later today, so any comments before then are definitely welcome.
Holy fracking cow Todd. Talk about great minds, eh? ;)
Word up :D
Thanks Ray, I'm glad it wasn't just me, I hate it when that happens. Your post pointed me in the right direction for handling this issue though. My solution ended up being similar to yours.
I add a header to the response with cfheader and in the javascript, I used ajaxSetup and in the complete option i just checked for the custom header like this:
if(XMLHttpRequest.getResponseHeader('error') != null && XMLHttpRequest.getResponseHeader('error') == 'SessionTimeout')
Follow up: http://www.coldfusionjedi.c...
Maybe this should go on my cfjedi wish list for 2011, but what about long-polling AJAX with ColdFusion. I think it's commonly called "comet".
If your clients support it, you could look into WebSocket. Nathan Mische has a CF Event Gateway for WebSockets up on RIAForge.
this method doesn't work if you are using cfajaxproxy to make your ajax calls, since cfajaxproxies don't have the X-Request-With in the header
You could use some other method probably.
not satisfied, but how could u bring an alert to ur regular user who were ideal during the session timeout.
its applicatikns responsibility to provide an alert prior to timeout.
how are we handling this in ur way ?
I'm sorry but I do not understand what you are asking. Can you rephrase it?
My regular ajax calls will have something like the following
ajax...{
},success:function(responseData){
// get the responseData and satisfy the business need
},error:function(a,b,c){
//like you said, we will get the error code and show the alert
}
Please correct me if am wrong from my above approach.
If am right, then like the above code snippet, i will have these snippets across the application every where.
Now do you mean to say that i have to place the "SessionTimeOut" code snippet in all the "error:function block ?"
Can you advice pls.
can you advice me on how to handle the AJAX when used from
xmlns:a4j="http://richfaces.org/a4j"
In all calls? No. Depending on your library, you can set up a global error handler for Ajax errors. jQuery makes this easy. Ditto for Angular.
Sorry - never seen that.