I?ve been encountering a problem as of late. I?m creating a lot of ?Single Page Pattern? web applications. This means you load the page once, then handle everything else via web service calls.
And if you have ever worked on web applications you should be familiar with the ?user went to lunch? issue (session timeout). User leaves for an hour, comes back to your web page, and expects it to continue working. Unfortunately the session timed out, and now you have to refresh a bunch o? crap. Another version of the problem, the ?.ASPXFORMSAUTH? cookies gets deleted. Either way, your web site stop just working and the user blames you.
The fix is to have the user log in again. Normally this is simple, when the user does something, then the page is refreshed and redirected to the login page (as assigned in the web.config). That is the standard web model. Unfortunately it does not work in this case.
Web services complicate things. When inside a web method request, redirects do not work, and even worse if you are using JQuery ?load? method (which loads html directly into an element), then you get the login page inside of a div. But more often, your web client code is expecting JSON, and will be getting html (an error page or the login page).
So my current solution it to wrap up the JQuery AJAX methods with my own methods. This way I can check the users authentication (another web method). If I see the user is not logged in, I redirect to the login page. The other web method is killed via the page redirect.
Here is my wrapper:
1: var ajax = {
2: _lastAuthCheck: new Date().getTime(),
3: getJSON: function(url, data, callback) {
4: ajax.CheckAuthentication();
5: return $.getJSON(url, data, callback);
6: },
7: post: function(url, data, callback, type) {
8: ajax.CheckAuthentication();
9: return $.post(url, data, callback, type);
10: },
11: load: function(destination, url, params, callback) {
12: ajax.CheckAuthentication();
13: destination.load(url, params, callback);
14: },
15: CheckAuthentication: function() {
16: if (!TimeToCheckAuth()) return;
17: $.getJSON('<%=Url.Action("IsAuthenticated", "Account")%>', null, function(result) {
18: if (!result) {
19: window.location = '<%=Url.Action("LogOn", "Account")%>';
20: } else {
21: ajax._lastAuthCheck = new Date().getTime();
22: }
23: });
24:
25: function TimeToCheckAuth(){
26: var currentTime = new Date().getTime();
27: var diff = currentTime - ajax._lastAuthCheck;
28:
29: return (diff > 5000);
30: }
31: }
32: };
Now with the wrapper in place I switch my $.ajax, $.get, $.post calls to ajax.ajax, ajax.get, ajax.post. All the parameters should stay the same.
Notice one thing, I?m mixing in Url.Action calls in that code. I should add that this code is in a separate file for me, not in my web form page. If you want to get more of an idea of how I do that, check out my post on Asp.Net MVC JavaScriptView.
Now the web method, IsAuthenticated, is about as simple as you can get:
1: [AcceptGet]
2: public JsonResult IsAuthenticated()
3: {
4: return Json(User.Identity.IsAuthenticated);
5: }
But back to the JavaScript, there are a couple of other things going on here. First off, the authentication check will happen at the same time actual web method call. I banking on the theory that the authentication check will return before the other method. Second, I put an extra check in the method, so at most the authentication check will only happen once ever 5 seconds. Adjust as you feel necessary.
One thing I would try is perhaps not to enforce the authentication on the ws using IIS but instead return some other json back which the client code can use to determine that it should show the login popup. Use a IsLoggedOut wrapper to parse the json first on the client. That way you don’t have two calls but just one.
-Mark
Did you try binding to the jquery events and vetoing/approving the xhr based on auth status?
I agree with Mark. I make one call that returns a ServiceCallResult object that has a result property to hold data coming back, a success property and a collection of status/error messages with codes that get populated when success is false to tell me (without details) what is wrong like a session timeout, not authenticated/authorized.
Simple but it works. If anyone has something different or better let’s hear it.
Hmm… I am wondering if there might be a custom HttpModule solution here that could reduce the code on each page, so that you could write the module once and not have to remember to use the special handling for each page of the application. Don’t get me wrong I like your solution, its a very smart way to handle putting the logic that deals with the login page in one place. The only problem I have is that it still has to be used for every page.
I would have opted for the simpler solution and just keep the user’s session alive as long as they do not navigate away from the site.
http://blechie.com/WPierce/archive/2007/11/26/Session-Keep-Alive-with-MonoRail-and-jQuery.aspx
-Bill
I try to avoid wrapping the jQuery calls if possible. Wrapping jQuery ajax means you’ll have problems with 3rd party plugins that use them.
As Mike suggested, I normally use the jQuery ajaxError event to detect a session expired and force a page reload (which will take me to the login page and redirect me back to the right place when I’m done).
For that to work I make my login page return an HTTP status code like 401-Unauthorized and check for that in the ajaxError event.
Hi guys,
Good suggestions all rounds. Pretty much proves I should have spent a little more time on the post. But I’ll look into what Sergio and Ben were talking about.
But a few things:
@John Sonmez: You shouldn’t have to write it for every page. Put the code in a js file and load it in a master page.
@Bill Pierce: Keeping the session alive is only a temporary fix. Do you keep it alive all night? All weekend? Each session is taking server resources. At some point you have to let the session die and move on. The other option is to abuse setInterval.
@Chris Brandsma
Oops sorry, I wasn’t clear enough. I meant the wrapped JQuery calls, but I suppose that is not a huge amount of overhead.
Wouldn’t this result in making two XHR calls for every request…one for the authentication check and another for the “real” request? Another option is to turn off Forms Authentication for all your web service URLs and then use a re-invention of the [Authorize] filter on those action methods that doesn’t redirect back to Login but instead sends some predefined JSON object indicating that the user is not authenticated or allows the actual method to be called. This results in a single XHR call and allows you to just handle the special authentication value in the direct jQuery method instead of wrapping it.
@JC Grubbs: at worst it would result in 2 calls for every request. But for my application, I tend to see a lot of calls in bursts. That is why I do a time check on my call.
Now, as for sending redefined JSON objects…I’m not sure I could do that without doing a LOT of extra work, especially for Load calls (which recieve HTML, not JSON). Plus, most of my get calls are already returning a lot of data. In fact, that is what half of my XHR calls are for — return data to the browse.
Good one