21

I have a view that loads a partial view from a controller action using jQuery. The controller action is decorated with the Authorize attribute and if the user has timed out when that action is called intead of being redirected to the proper LogOn page, the LogOn page gets inserted in the view where the partial view would have gone.

There is another post here that describes a solution using jQuery but unfortunately it doesn't include any code samples which is what I need because I'm not very familiar with jQuery (and new to web development) and I need to get this issue resolved as soon as I can.

All I want to do is redirect the user to the proper LogOn page which sounds pretty straight forward to me but so far it doesn't seem to be. If jQuery is the way to go, could someone please post a code sample, that would really help me out.

Thanks.

Community
  • 1
  • 1
fynnbob
  • 553
  • 1
  • 5
  • 14
  • "sounds pretty straight forward to me but so far it doesn't seem to be" Sounds like you've already coded something. You should post what you've done so far so people can help fix what you've written. You are never going to get better with jquery or web dev if your just pasting in source code all the time. – John Farrell Nov 16 '10 at 20:48
  • What I meant by that statement was that from what I've researched it does not seem to be as straight forward as I thought it should be. In fact, the only post I found directly related to what I want to do is the one I mentioned and since no code was given I would need to spend time researching how to implement it. I was hoping that there would be some simple mechanism to tell the client to "forget the current page and go to this one". I tried a few different redirects but the result was always the same. – fynnbob Nov 17 '10 at 14:30

2 Answers2

34

Solution 1, return response indicating error


Pro ASP.NET MVC 2 Framework offers one solution for this

If an Ajax request is denied authorization, then usually you don’t want to return an HTTP redirection to the login page, because your client-side code is not expecting that and may do something unwanted such as injecting the entire login page into the middle of whatever page the user is on. Instead, you’ll want to send back a more useful signal to the client-side code, perhaps in JSON format, to explain that the request was not authorized. You could implement this as follows:

public class EnhancedAuthorizeAttribute : AuthorizeAttribute 
{ 
    protected override void HandleUnauthorizedRequest(AuthorizationContext context) 
    { 
        if (context.HttpContext.Request.IsAjaxRequest()) { 
            UrlHelper urlHelper = new UrlHelper(context.RequestContext); 
            context.Result = new JsonResult { 
                Data = new { 
                    Error = "NotAuthorized", 
                    LogOnUrl = urlHelper.Action("LogOn", "Account") 
                }, 
                JsonRequestBehavior = JsonRequestBehavior.AllowGet 
            }; 
        } 
        else 
            base.HandleUnauthorizedRequest(context); 
    } 
} 

You'd then get back a JSON object from the controller {'Error': 'NotAuthorized', 'LogonUrl': '...'} which you could then use to redirect the user.

And alternative response, if you're expecting HTML, could be to return a plain string, like NotAuthorized:<your_url> and check if the response matches this pattern.


Solution 2, return and handle 401 Unautorized status code


As this has to be checked on every ajax request's success callback, this gets quite tedious. It would be nice to be able to catch this case globally. The best solution would be to return a 401 Unauthorized status code from the server and use jQuery's .ajaxError to catch it, but this is problematic on IIS/ASP.NET, since it's deadly committed to redirecting you to the login page if the response has this statuscode. As long as the <authentication> tag is present in web.config this redirection will happen [1] if you don't do something about it.

So let's do something about it. This hacky way seemed nice. In your global.asax.cs

protected void Application_EndRequest()
{
    if (Context.Response.StatusCode == 302 && Context.Request.RequestContext.HttpContext.Request.IsAjaxRequest())
    {
        Context.Response.Clear();
        Context.Response.StatusCode = 401;
    }
}

(I'd love to hear it if anyone knows of any better methods to return a 401 status code.)

By doing this, you're preventing the default behaviour of redirecting to the logon page when the request is an ajax request. You can therefore use the default AuthorizeAttribute as it is, since Application_EndRequest takes care of the rest.

Now, in the jQuery code we'll use the .ajaxError function to catch the error. This is a global ajax event handler, meaning it will intercept every error made by any ajax call. It can be attached to any element - in my example I've just chosen body cause its always present

$("body").ajaxError(function(event, XMLHttpRequest, ajaxOptions, thrownError) {
    if (XMLHttpRequest.status == 401) {
        alert("unauthorized");
    }
});

This way you get your redirection logic centralized in one place, instead of having to check it on every damn ajax request success callback


Solution 3, return custom header


This answer offers an alternative solution. It uses the same kind of global event handler, but skips the hacky bits. It adds a custom header to the page if you're not authenticated and the request is an ajax request, and checks for this header on each .ajaxComplete. Note that the method provided in the linked answer is unsecure, as it will return the original view with possibly sensitive content and rely on javascript to redirect the user away from it. With a bit of modification it's quite awesome as it doesn't require any hacks and the logic can be centralized to one callback function. The only downside as I can see is that it doens't reply with the semantically correct status code, as it's actually not 200 OK because you're 401 Unauthorized


We can bake this into another custom EnhancedAuthorizationAttribute

public class EnhancedAuthorizeAttribute : AuthorizeAttribute
{
        protected override void HandleUnauthorizedRequest(AuthorizationContext context)
        {
            if (context.HttpContext.Request.IsAjaxRequest()) 
            {
                context.HttpContext.Response.AddHeader("REQUIRES_AUTH", "1");
                context.Result = new EmptyResult();
            }
            else
            {
                base.HandleUnauthorizedRequest(context);
            }
        }
    }
}

Now, every time an ajax request completes, you check for this header:

$('body').ajaxComplete(function(event,request,settings){
    if (request.getResponseHeader('REQUIRES_AUTH') === '1'){
        alert("unauthorized");
    };
});
Community
  • 1
  • 1
Simen Echholt
  • 11,243
  • 2
  • 34
  • 26
  • +1, however, bear in mind that this will need to be handled in the success handler of the ajax request, as the response code will be 200. As stated in the HttpUnauthorizedResult class (mvc source code), FormsAuthenticationModule looks for 401 responses and instead redirects the user to the login page. This is why the answer provided in the question that @fynnbob mentioned is not correct. If you set a status code 401 in the Response, the client will not see that response code and hence it will not hit the ajax error handler. – uvita Nov 17 '10 at 02:49
  • Thanks for the responses. I'm actually using jQuery to load the partial (eg. $("#salescharts").load('/Sales/SalesByProduct?productId=12345');). I will try this approach and update the post when I'm done. – fynnbob Nov 17 '10 at 19:41
  • @uvita: Updated my answer with a hacky way to enable returning 401. @fynnbob Handling this with the .ajaxError function allows you to keep using .load ect and not be forced to use the lower level functions – Simen Echholt Nov 18 '10 at 01:39
  • Fantastic options! Because of time constraints I implemented a very hacky solution which was to extend the AuthorizeAttribute to return a string ("Unauthorized") which I look for in the response and then redirect the user if I find it. Only actions that return partial views (which are not many) are decorated with this attribute. The options provided here are a much better approach and have been very informative so when I have more time I am definitely going to redo my code. Thanks for the awesome help! – fynnbob Nov 19 '10 at 14:56
  • Could you just `context.Result = new EmptyResult();` for solution 3? – ajbeaven Sep 29 '11 at 22:09
  • Excellent answer! Solution seems the way to go :) – Fabio Milheiro Nov 29 '11 at 00:15
  • An alternative and perhaps more explicit way to test for a redirection is 'Context.Response.IsRequestBeingRedirected'. Also, could use '(int)HttpStatusCode.Unauthorized' in place of magic number 401. – Stuart Hallows May 07 '13 at 01:09
  • 1
    +1 , just to note, `As of jQuery 1.8, the .ajaxComplete() method should only be attached to document.` – VladL Mar 13 '14 at 10:40
3

For Simen Echholt second solution you can use

$.ajaxSetup({ global: true,
    statusCode: {
        401: function () {
            alert("unauthorized");
        }
    }
});

to bind some your function on status code 401

Mykola
  • 31
  • 1