Cloud

Gracefully handle MVC login session expiration in javascript

If your web application is built using ASP.NET MVC stack and it requires user authentication and authorization to access a certain parts of the application (or application as a whole), then the chances are that you using [Authorize] controller attribute. This attribute could be applied to controller as a whole or to any of the controller actions and it acts as a request pre-filter, checking if user is authorized, and if not then directing user to the login page.

The [Authorize] attribute is working great for a traditional MVC application where web page content is refreshed by reloading a whole page. But let’s say that you have a single-page application or hybrid page where parts of your pages are served by javascript code which is talking to your application controller asynchronously, using AJAX. Will [Authorize] attribute work well for such controller methods? Not that much. Let’s see what’s happening inside [Authorize] attribute and what it’s returning to the client/browser… session_expired


Lets consider the following setup:

public class Account: Controller
{
   [HttpGet]
   public ActionResult Login(string returnUrl)
  {
    ....
  }
}
[Authorize]
public class Home: Controller
{
   [HttpGet]
   public ActionResult Index()
  {
    return View();
  }
   [HttpPost]
   public ActionResult GetData()
  {
    return Json("hello world!");
  }
}
Microsoft - The Essential Guide to Microsoft Teams End-User Engagement
The Essential Guide to Microsoft Teams End-User Engagement

We take you through 10 best practices, considerations, and suggestions that can enrich your Microsoft Teams deployment and ensure both end-user adoption and engagement.

Get the Guide

When browser is hitting /home/index and user is not authorized, then [Authorize] filter is intercepting the request and be returning 302 (temporary redirect) response code:

HTTP/1.1 302 Found
Cache-Control: private
Content-Type: text/html; charset=utf-8
Location: /Account/Login?ReturnUrl=%2f

If user logged and and then let the session expire then result will be the same, because user is no longer authorized after session is expired. Browser will understand the 302 response and redirect user to the login page.
So, how the GetData action which is returning JSON result for javascript consumption will be handled by [Authorize] attribute? Exactly the same, of course. If user waited for 20 minutes or longer and then performed an action on the page which is resulting in the AJAX call to GetData method then [Authorize] will return 302 code and then Login page as a response body (as HTML).
But it’s not exactly that your javascript code is expecting, most likely. The chances are that your javascript code is expecting a JSON object in return, but in this case it’s not going to happen. Javascript will get a HTML response body instead of the JSON and likely will choke on it.
So, what’s a better way to handle this situation inside javascript? First of all, 302 response is not quite appropriate there. While it’s very friendly for the browser which will use it as a guidance to send user to login page, jQuery (I assume we using $.ajax or $.getJSON methods to call GetData action) will not interpret it automatically. In this case 401 (unauthorized request) seems to be more appropriate. In order to handle this kind of response, let’s create our own version of authorization filter by subclassing the default filter:

     /// <summary>
    /// Extend AuthorizeAttribute to correctly handle AJAX authorization
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class MyAuthorizeAttribute : AuthorizeAttribute
    {
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                filterContext.HttpContext.Response.StatusCode = 401;
                filterContext.Result = new JsonResult
                {
                    Data = new
                    {
                        Error = “NotAuthorized”,
                        LogOnUrl = FormsAuthentication.LoginUrl
                    },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };
                filterContext.HttpContext.Response.End();
            }
            else
            {
              // this is a standard request, let parent filter to handle it
               base.HandleUnauthorizedRequest(filterContext);
            }
        }
    }

Now, we need to modify our client script to handle 401 response code:

getData = function(request) {
    $.ajax({
        url: “/home/GetData”,
        type: “POST”,
        data: request,
        contentType: “application/json;”,
        dataType: “json”,
        success: function (repJSON) {
        },
        error: function (xhr) {
            if (xhr.status === 401) {
                window.location.href=xhr.Data.LogOnUrl;
                return;
            }
        }
    });

This is it. Now we need to decorate our controller with [MyAuthorize] and use the code above for AJAX calls, they’ll be handling session expiration gracefully.

About the Author

Solutions Architect at Perficient

More from this Author

Thoughts on “Gracefully handle MVC login session expiration in javascript”

  1. Victor Corrales

    Hello, just a question, I tried the code but the object xhr.Data.LogOnUrl is null, if you see the Response object in the browser it comes empty, the Header is fine with the error 401. Any idea about it ?
    Thanks !

  2. Stan Tarnovskiy

    Check your forms authentication settings (in web.config). Most likely your LoginUrl is not set. If you using other authentication methods (like windows authentication, for example), or you prefer not to configure LoginUrl, then you can set this url either in HandleUnauthorizedRequest method, or in javascript. Basically, all the you need is to redirect user to the login page somehow.

  3. I’m using MVC and I had to modify/add the following code to make this work:
    I removed the following line:
      filterContext.HttpContext.Response.End();
    I added the following to the CookieAuthenticationOptions:
    OnApplyRedirect = ctx => {
    if (!IsAjaxRequest(ctx.Request)) {
    ctx.Response.Redirect(ctx.RedirectUri);
    }
    }
    private static bool IsAjaxRequest(IOwinRequest request)
    {
    IReadableStringCollection query = request.Query;
    if ((query != null) && (query[“X-Requested-With”] == “XMLHttpRequest”)) {
    return true;
    }
    IHeaderDictionary headers = request.Headers;
    return ((headers != null) && (headers[“X-Requested-With”] == “XMLHttpRequest”));
    }

  4. Hello Stan Tarnovskiy,
    i wrote the exact code in AuthorizeAttribute and tried to catch the error both globally and one function only like
    $(document).ajaxError(function (e, xhr, opt) {
    if (xhr.status === 401) {
    window.location.href = xhr.Data.LogOnUrl;
    return;
    }
    });
    $.ajax({
    type: “POST”,
    url: saveurl,
    data: { objOrganisation: OrgObj[0] },
    dataType: “json”,
    success: function (data) {
    if (data.errMsg) {
    popModal(data.errMsg, “Error”)
    }
    else {
    popModal(“Data Saved Successfully”, “Save”);
    $(“#btnclosemsg”).click(function () {
    location.reload();
    });
    $(“#btnclosemsgX”).click(function () {
    location.reload();
    });
    }
    },
    error: function (xhr) {
    if (xhr.status === 401) {
    window.location.href = xhr.Data.LogOnUrl;
    return;
    }
    });
    but both are not working, always getting xhr.status =200
    Can you please explain why this happening and any solution for this???
    I am using MVC5-razor
    Thanks in Advance

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up