Jquery – AJAX and FormsAuthentication, how prevent FormsAuthentication overrides HTTP 401

ajaxasp.netforms-authenticationhttpjquery

In one application configured with FormsAuthentication, when a user access without the auth cookie or with an outdated one to a protected page, ASP.NET issue a HTTP 401 Unauthorized, then the FormsAuthentication module intercepts this response before the request end, and change it for a HTTP 302 Found, setting a HTTP header "Location: /path/loginurl" in order to redirect the user agent to the login page, then the browser goes to that page and retrieves the login page, that is not protected, getting an HTTP 200 OK.

That was a very good idea indeed, when AJAX was not being considered.

Now I have a url in my application that returns JSON data and it needs the user to be authenticated. Everything works well, the problems is that if the auth cookie expires, when my client side code call the server it will get a HTTP 200 OK with the html of the login page, instead a HTTP 401 Unauthorized (because the explained previously). Then my client side is trying to parse the login page html as json, and failing.

The question then is : How to cope with an expired authentication from client side? What is the most elegant solution to cope with this situation? I need to know when the call has been successful or not, and I would like to do it using the HTTP semantic.

Is it possible to read custom HTTP Headers from client side in a safe cross browser way?
Is there a way to tell the FormsAuthenticationModule to not perform redirections if the request is an AJAX request?
Is there a way to override the HTTP status using a HTTP header in the same way you can override the HTTP request method?

I need the Forms authentication, and I would like to avoid rewrite that module or write my own form authentication module.

Regards.

Best Answer

I had the same problem, and had to use custom attribute in MVC. You can easy adapt this to work in web forms, you could override authorization of your pages in base page if all your pages inherit from some base page (global attribute in MVC allows the same thing - to override OnAuthorization method for all controllers/actions in application)

This is how attribute looks like:

public class AjaxAuthorizationAttribute : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest()
                && !filterContext.HttpContext.User.Identity.IsAuthenticated
                && (filterContext.ActionDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0
                || filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0))
            {
                filterContext.HttpContext.SkipAuthorization = true;
                filterContext.HttpContext.Response.Clear();
                filterContext.HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
                filterContext.Result = new HttpUnauthorizedResult("Unauthorized");
                filterContext.Result.ExecuteResult(filterContext.Controller.ControllerContext);
                filterContext.HttpContext.Response.End();
            }
        }
    }

Note that you need to call HttpContext.Response.End(); or your request will be redirected to login (I lost some of my hair because of this).

On client side, I used jQuery ajaxError method:

var lastAjaxCall = { settings: null, jqXHR: null };
var loginUrl = "yourloginurl";

//...
//...

$(document).ready(function(){
    $(document).ajaxError(function (event, jqxhr, settings) {
            if (jqxhr.status == 401) {
                if (loginUrl) {
                    $("body").prepend("<div class='loginoverlay'><div class='full'></div><div class='iframe'><iframe id='login' src='" + loginUrl + "'></iframe></div></div>");
                    $("div.loginoverlay").show();
                    lastAjaxCall.jqXHR = jqxhr;
                    lastAjaxCall.settings = settings;
                }
            }
    }

}

This showed login in iframe over current page (looking like user was redirected but you can make it different), and when login was success, this popup was closed, and original ajax request resent:

if (lastAjaxCall.settings) {
        $.ajax(lastAjaxCall.settings);
        lastAjaxCall.settings = null;
    }

This allows your users to login when session expires without losing any of their work or data typed in last shown form.