views:

478

answers:

4

How do you handle ajax requests when user is not authenticated?

Someone enters the page, leaves room for an hour, returns, adds comment on the page that goes throuh ajax using jQuery ($.post). Since he is not authenticated, method return RedirectToRoute result (redirects to login page). What do you do with it? How do you handle it on client side and how do you handle it in controller?

+3  A: 

The idea I came up with when a coworker asked about how to handle it was this - make an AuthorizeAjax attribute. It can interrogate and verify that Request.IsAjaxRequest() and, if the request isn't authenticated, return a specific JSON error object. It's possible you could simply override the default AuthorizeAttribute and have it call the base unless it's an unauthorized AJAX request so you don't have to worry about whether to tag controller actions with [Authorize] or [AuthorizeAjax].

On the client-side, all your pages would have to be equipped to deal with the returned error, but that logic can likely be shared.

48klocs
How do you share it? How does your AuthorizeAjax attribute look like?
LukLed
I can't share that, but ASP.Net MVC is open source, so you can take a look at how they implemented it for ideas. As far as the client-side sharing goes, you can deal by binding a central function (a dialog or alert or whatever) to the AJAX error event (http://docs.jquery.com/Ajax_Events).
48klocs
+1  A: 

I would propose creating your own AuthorizeAttribute and if the request is an Ajax request, throw an HttpException(401/403). And also switch to use jQuery's Ajax Method instead.

Assuming you've implemented error pages and they return the correct status code, the error callback will be executed instead of the success callback. This will be happen because of the response code.

Jab
+2  A: 

While I like the ideas posted in other answers (which I had idea about earlier), I needed code samples. Here they are:

Modified Authorize attribute:

public class OptionalAuthorizeAttribute : AuthorizeAttribute
{
    private class Http403Result : ActionResult
    {
        public override void ExecuteResult(ControllerContext context)
        {
            // Set the response code to 403.
            context.HttpContext.Response.StatusCode = 403;
            context.HttpContext.Response.Write(CTRes.AuthorizationLostPleaseLogOutAndLogInAgainToContinue);
        }
    }

    private readonly bool _authorize;

    public OptionalAuthorizeAttribute()
    {
        _authorize = true;
    }

    //OptionalAuthorize is turned on on base controller class, so it has to be turned off on some controller. 
    //That is why parameter is introduced.
    public OptionalAuthorizeAttribute(bool authorize)
    {
        _authorize = authorize;
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        //When authorize parameter is set to false, not authorization should be performed.
        if (!_authorize)
            return true;

        var result = base.AuthorizeCore(httpContext);

        return result;
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            //Ajax request doesn't return to login page, it just returns 403 error.
            filterContext.Result = new Http403Result();
        }
        else
            base.HandleUnauthorizedRequest(filterContext);
    }
}

HandleUnauthorizedRequest is overridden, so it returns Http403Result when using Ajax. Http403Result changes StatusCode to 403 and returns message to the user in response. There is some additional logic in attribute (authorize parameter), because I turn on [Authorize] in base controller and disable it in some pages.

Other important part is global handling of this response on client side. This is what I placed in Site.Master:

<script type="text/javascript">
    $(document).ready(
        function() {
            $("body").ajaxError(
                function(e,request) {
                    if (request.status == 403) {
                        alert(request.responseText);
                        window.location = '/Logout';
                    }
                }
            );
        }
    );
</script>

I place GLOBAL ajax error handler and when evert $.post fails with 403 error, response message is alerted and user is redirected to logout page. Now I don't have to handle error in every $.post request, because it is handled globally.

Why 403, not 401? 401 is handled internally by MVC framework (that is why redirection to login page is done after failed authorization).

What do you think about it?

LukLed
Funny that microsoft did not provide a default handler like this for ajax requests.
Cherian
@Cherian: Microsoft heavily promotes jQuery with ASP.NET, so it not that weird. Microsoft AJAX is still here, because many people got used to it with ASP.NET Webforms, but I believe jQuery is preferred way.
LukLed
I didn't mean that. jQuery still is the way. i meant why didn't they have an "AjaxAuthorize(return="json")" which overrides their 401 handling and gives us probably { success : "false" , status = 401 }
Cherian
@Cherian: `Authorize` attribute returns 401 in case of lack of authorization. That is all you need, status says everything. There is no need for additional json. Then problem is that when you use forms authentication, 401 is handled on server side and redirected to login page. It doesn't make much sense with ajax requests.
LukLed
exactly my point. almost everyone needs `<authentication mode="Forms">`
Cherian
A: 

The simplest and cleanest solution I've found for this is to register a callback with the jQuery.ajaxSuccess() event and check for the "X-AspNetMvc-Version" response header.

Every jQuery Ajax request in my app is handled by Mvc so if the header is missing I know my request has been redirected to the login page, and I simply reload the page for a top-level redirect:

 $(document).ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
    // if request returns non MVC page reload because this means the user 
    // session has expired
    var mvcHeaderName = "X-AspNetMvc-Version";
    var mvcHeaderValue = XMLHttpRequest.getResponseHeader(mvcHeaderName);

    if (!mvcHeaderValue) {
        location.reload();
    }
});

The page reload may cause some Javascript errors (depending on what you're doing with the Ajax response) but in most cases where debugging is off the user will never see these.

If you don't want to use the built-in header I'm sure you could easily add a custom one and follow the same pattern.

Ashley Tate