There’s a lot more than meets the eye when you need to handle session and authentication timeout scenarios in ASP.NET MVC. For some reason, I expected this to be a no-brainer when I first worked on an app that needed this functionality. Turns out there several complications that we need to be aware of. On top of that, be prepared for the potential of a lot of test points on a single page.

Server Timeout Checks

We’ll create a couple of action filters to provide cross-cutting checks for timeout scenarios. The first will normally be hit when the browser session has timed out (because I’d set that to a shorter time span than authentication), but will also handle if the authentication has timed out first:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class SessionExpireFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        HttpContext ctx = HttpContext.Current;

        // If the browser session or authentication session has expired...
        if (ctx.Session["UserName"] == null || !filterContext.HttpContext.Request.IsAuthenticated)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                // For AJAX requests, we're overriding the returned JSON result with a simple string,
                // indicating to the calling JavaScript code that a redirect should be performed.
                filterContext.Result = new JsonResult { Data = "_Logon_" };
            }
            else
            {
                // For round-trip posts, we're forcing a redirect to Home/TimeoutRedirect/, which
                // simply displays a temporary 5 second notification that they have timed out, and
                // will, in turn, redirect to the logon page.
                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary {
                        { "Controller", "Home" },
                        { "Action", "TimeoutRedirect" }
                });
            }
        }

        base.OnActionExecuting(filterContext);
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class LocsAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        HttpContext ctx = HttpContext.Current;

        // If the browser session has expired...
        if (ctx.Session["UserName"] == null)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                // For AJAX requests, we're overriding the returned JSON result with a simple string,
                // indicating to the calling JavaScript code that a redirect should be performed.
                filterContext.Result = new JsonResult { Data = "_Logon_" };
            }
            else
            {
                // For round-trip posts, we're forcing a redirect to Home/TimeoutRedirect/, which
                // simply displays a temporary 5 second notification that they have timed out, and
                // will, in turn, redirect to the logon page.
                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary {
                        { "Controller", "Home" },
                        { "Action", "TimeoutRedirect" }
                });
            }
        }
        else if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            // Otherwise the reason we got here was because the user didn't have access rights to the
            // operation, and a 403 should be returned.
            filterContext.Result = new HttpStatusCodeResult(403);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

As you can see, for both attributes we’re using a session variable holding the user name as an indication if a session timeout occurred. We’re checking to see if either the browser session or the authentication has expired. I like to set the browser session to a shorter time period than authentication, because I end up running into extra issues to code around if the authentication expires first and the session is still active.

Then we’re checking if this is an AJAX request. Since we cannot immediately redirect upon such a request, we instead return a JSON result containing the string “_Logon_”. Later, within a JavaScript function, we’ll check for this as one of the possible values used to determine if a timeout occurred.

By the way, in the second attribute, HandleUnauthorizedRequest, we’re handling unauthorized scenarios different from timeouts (which is, unfortunately, how MVC 3 handles it out of the box). I got this idea from this article on StackOverflow. I believe the next version of MVC is supposed to provide better control for this by default.

The Timeout Warning Message Page

If this wasn’t an AJAX request, we simply redirect to a /Home/TimeoutRedirect page, which briefly displays a message explaining to the user that their session timed out, and that they’ll be redirected to the logon page. We use the meta tag redirect (after 5 seconds) in this view:

<meta http-equiv="refresh" content="5;url=/Account/Logon/" />

<h2>
    Sorry, but your session has timed out. You'll be redirected to the Log On page in 5 seconds...
</h2>

The JavaScript Check

The following JavaScript function would be called in the success, error, and complete callback functions on a jQuery.Ajax call. We use it to check if the response returned an indication that a timeout occurred, before attempting to process. It assumes that the parameter, data, is passed in from the AJAX call response.

This function expects that one of three returned values indicate a timeout occurred:

  1. A redirect was already attempted by the controller, likely due to an authentication timeout. Since an AJAX response is usually expecting a JSON return value, and since the redirect is attempting to return the full actual Log On page, this function checks the responseText for the existence of an HTML <title> of “Log On” (the default log on page title in an MVC app).
  2. A redirect is in the process of being attempted by the controller, likely due to an authentication timeout. Since an AJAX response is usually expecting a JSON return value, and since the redirect is attempting to return a full redirect (302) info page, this function checks the responseText for the existence of an HTML <title> of “Object moved” (the default 302 page title).
  3. If a session timeout occurred, the value “_Logon_” should be returned by the controller action handling the AJAX call. The above action filters check to see if the session variable “UserName” is null, which would indicate a session timeout, but not necessarily an authentication timeout.

This function also expects an AJAX action handler called TimeoutRedirect, on the Home controller. If you use a different controller or action, you’ll need to modify the URL specified in the function. The parameter, data, should be the response from an AJAX call attempt.

function checkTimeout(data) {
    var thereIsStillTime = true;

    if (data) {
        if (data.responseText) {
            if ((data.responseText.indexOf("<title>Log On</title>") > -1) || (data.responseText.indexOf("<title>Object moved</title>") > -1) || (data.responseText === '"_Logon_"')) thereIsStillTime = false;
        } else {
            if (data == "_Logon_") thereIsStillTime = false;
        }

        if (!thereIsStillTime) {
            window.location.href = "/Home/TimeoutRedirect";
        }
    } else {
        $.ajax({
            url: "/Home/CheckTimeout/",
            type: 'POST',
            dataType: 'json',
            contentType: 'application/json; charset=utf-8',
            async: false,
            complete: function (result) {
                thereIsStillTime = checkTimeout(result);
            }
        });
    }

    return thereIsStillTime;
}

The Forced AJAX Attempt

There may be times you want to check for a timeout scenario even if your app doesn’t require an AJAX call. That’s why the function is written so that if no parameter is passed in, a simple AJAX call will be made, forcing communication with the server in order to get back session and authentication information, so we can see if a timeout had occurred. There’s no way a browser would know this information until communication with the server is attempted. Once that AJAX call is made, this function will call itself with an actual data value that can now be interrogated.

Client-Side Calling Code Sample

The function returns true if no timeout occurred yet. We simply execute our callback logic if the result of this call is true (no timeout occurred):

$.ajax({
    url: "/MyController/MyAction",
    type: 'POST',
    dataType: 'json',
    data: jsonData,
    contentType: 'application/json; charset=utf-8',
    success: function (result) {
        if (checkTimeout(result)) {
            // There was no timeout, so continue processing...
        }
    },
    error: function (result) {
        if (checkTimeout(result)) {
            // There was no timeout, so continue processing...
        }
    }
});

Again, if you want to check for a timeout where no AJAX call is needed, such as for a click event when the user is navigating a list box, just call checkTimeout() with no parameter. Just note that a simple AJAX call will be injected, so be aware of potential performance impacts, and don’t overuse this. Also, be aware that some browsers, such as IE, will automatically cache AJAX results, and the call may not be made (and, therefore, the timeout check won’t occur). You may have to turn off AJAX caching ($.ajaxSetup({ cache: false })) in this case.

If you have any improvements on this, please post a comment. I’m always looking to tweak this. Thanks.