Jquery – Handling session timeout in ajax calls

asp.net-mvcasp.net-mvc-3authorize-attributejquery

I'm making an ajax call using jquery to an asp.net mvc controller action:

[AcceptVerbs(HttpVerbs.Post)]
        public ActionResult GetWeek(string startDay)
        {
            var daysOfWeek = CompanyUtility.GetWeek(User.Company.Id, startDay);
            return Json(daysOfWeek);
        }

When session times out, this call will fail, as the User object is stored in session. I created a custom authorize attribute in order to check if session was lost and redirect to the login page. This works fine for page requests, however it doesn't work for ajax requests, as you can't redirect from an ajax request:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AuthorizeUserAttribute : AuthorizeAttribute
    {
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (!httpContext.Request.IsAjaxRequest())
            {//validate http request.
                if (!httpContext.Request.IsAuthenticated
                    || httpContext.Session["User"] == null)
                {
                    FormsAuthentication.SignOut();
                    httpContext.Response.Redirect("~/?returnurl=" + httpContext.Request.Url.ToString());
                    return false;
                }
            }
            return true;
        }
    }

I read on another thread that when the user isn't authenticated and you make an ajax request, you should set the status code to 401 (unauthorized) and then check for that in js and redirect them to the login page. However, I can't get this working:

protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (Request.IsAjaxRequest() && (!Request.IsAuthenticated || User == null))
            {
                filterContext.RequestContext.HttpContext.Response.StatusCode = 401;
            }
            else
            {
                base.OnActionExecuting(filterContext);
            }
        }

Basically, it'll set it to 401, but then it'll continue into the controller action and throw an object ref not set to an instance of an object error, which then returns error 500 back to the client-side js. If I change my custom Authorize attribute to validate ajax requests as well and return false for those that aren't authenticated, that makes the ajax request return my login page, which obviously doesn't work.

How do I get this working?

Best Answer

You could write a custom [Authorize] attribute which would return JSON instead of throwing a 401 exception in case of unauthorized access which would allow client scripts to handle the scenario gracefully:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new JsonResult
            {
                Data = new 
                { 
                    // put whatever data you want which will be sent
                    // to the client
                    message = "sorry, but you were logged out" 
                },
                JsonRequestBehavior = JsonRequestBehavior.AllowGet
            };
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

then decorate your controller/actions with it and on the client:

$.get('@Url.Action("SomeAction")', function (result) {
    if (result.message) {
        alert(result.message);
    } else {
        // do whatever you were doing before with the results
    }
});