Handling Session and Authentication Timeouts in ASP.NET MVC

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.

Similar Posts:

image_pdf

Comments

  1. Shane C says

    What do you use SessionExpireFilterAttribute for? Session["UserName"] is always null for me.

  2. says

    Hi Shane. I should have mentioned this in the post: I set the Session["UserName"] to User.Identity.Name in the AccountController Logon post action, if the logon was successful. From that point on, if the session is still active, Session["UserName"] won’t be null.

    The SessionExpireFilterAttribute action filter is then automatically called before each action to check if Session["UserName"] is null (plus some other factors indicating a timeout). If it determines a timeout has not occurred, it allows the actual called action to execute. Otherwise, it forces a redirect to a timeout notification page, which in turn redirects to the logon page to allow the user to re-logon.

  3. ISMAIL ONEL says

    Hello, thanks for the post.
    But I couldn’t get it work. Could you add a sample project please?

  4. Kehinde says

    Thanks for this explanation. I deeply appreciate this. Please, Is it possible to get the codes packaged together in a zip file on this site?

  5. says

    Although I’d like to put together a sample app for this, I’m in the middle of a big move, and won’t be able to look at this for another month or so.

  6. Patrick says

    would it not just be better to have an action filter attribute checking for session/authentication timeouts , then returning the raw http status code 403/401 . Then handle this globally on the client using jquery :
    $.ajaxSetup({ cache: false,
    error: function (xhr, status, err) {
    if (xhr.status == 401)
    window.location.href = ‘@(Html.Action(“LogOn”,”Account”,”"))’;
    }
    });

  7. krishna kanth says

    this is worked for me…i have no problems iam ready to post the zip file .if anybody needs that get the process so i can upload my code.

  8. vamshi says

    Hi,
    Awesome very useful information. i am new to MVC. Any help would be appreciated.
    I need some clarification.
    1. Javascript check and clientside calling sample code, where do you include them in project.
    2. Browser timeout? I want to set the timeout manually to 20 minutes and display the metatag message for 1minute with options to stay in or logout (2 buttons).
    Please help me

  9. says

    Very good article. Thank you for the help. I will use it with some modifications such as I have added SessionVariable property that is used to check a session variable instead of constant “UserName”. For exaple; [SessionExpireFilter(SessionVariable="UserContext")] Or adding ReturnUrl querystring variable to ActionResult for coming back to the same address after login.

  10. Osk says

    Hi Mark,
    thanks a lot for the article, i am fighting with this issue at this very moment, however, I am running into an issue, that I hope you can help me with. I am using MVC 4, after the user has logged successfully, i put the session variable, but when I call the login page for the first time, the filter catch that request BEFORE hitting the login action, since there is no session variable, it will redirect to the login page again, thus creating and endless loop… do you know why this happen? or why did this not happen to you? any help would be greatly appreaciated….

    Osk

  11. says

    Hi Osk,

    I should have mentioned this in the article — don’t apply the attribute to the LogOn action (or the AccountController). No need for it there. Apply it to all other controllers where you want to handle timeouts.

    = Mark

  12. says

    I know I really need to create a sample app. I pulled the sample code from a large app I was working on for a client, so I need to write a sample from scratch due to the proprietary nature of the client’s app.

  13. says

    Excellent blog article.
    Pls beware that the asp.net mvc will generate a new session for every request unless there is something stored in the Session variable. This can be anything (you’re setting UserName in your code) but it needs to be there – otherwise it becomes impossible to distinguish session timeouts from any other request.
    Ismar

  14. says

    Hi Ismar. Are you sure about that? A session ID (Session.SessionID) is automatically generated on the server when the first page is requested, and retained for subsequent requests. Unless I’m misunderstanding something, you really don’t need to add your own session variable to keep the session alive, unless you disable session state on an application level by setting the <sessionstate> mode value off in web.config, or at the page level via the EnableSessionState=”false” page directive.

  15. says

    Thanks, Ismar. I stand corrected. I have to look into this deeper to find out why they generate new sessions so frequently. It seems contradictory to what I’d expect.

  16. Ricardo Aranibar says

    krishna kanth and Mark Freedman

    If you can upload your code, I will be grateful.
    please tell me where you post the zip file.

  17. Kevin says

    Mark, I’m using a modified version of this code in one of our web applications. Thanks for sharing your discoveries when working with this issue. One thing I’ve noted though is that the redirect in OnActionExecuting when session username is null seems to effectively disable MVC4′s “AllowAnonymous” filter. It checks for a valid session by looking at something that requires a login, meaning if you’re not logged in the session is invalid, even if the method you’re executing is AllowAnonymous. Am I misunderstanding this, or is there a way you can think of to work around this issue?

  18. says

    Ricardo, Krishna mentioned a zip file back in late 2012, but I haven’t heard anything since. I’m rethinking my solution, so I don’t have source code examples other than what I posted here.

  19. says

    Good question, Kevin. The most recent application I’m working on that makes use of this technique is still under MVC3. We plan on converting it to MVC5 soon, so I may run into the same issue. I’ll have to revisit it then. I’m not sure about a workaround yet. If you find out before I do, please let me know. I’ve been rethinking this approach lately, anyway. It’s a couple of years old, and may need a review.

  20. Kevin says

    I’d be interested in what you discover on this issue when you move to MVC5, or if you have other updated thoughts on this. It’s not perfect, and neither are my tweaks to it for MVC4, but by adapting it a little it has served us pretty well so far. You’re right; it is a more difficult situation than meets the eye.

    I worked around the “AllowAnonymous” issue for now by checking for that attribute, and if found, bypassing the session checking logic:

    if (filterContext.ActionDescriptor.GetCustomAttributes(typeof(AllowAnonymousAttribute), false).Any())
    {
    // do the stuff
    }

    Not elegant at all, but at least addresses the immediate issue for us. Thanks again and look forward to further discoveries you might make on this topic.

Leave a Reply

Your email address will not be published. Required fields are marked *