Introduction
For modern web-applications has become the usual to use AJAX when you create user interfaces. However, it makes our headache from time to time. And often these difficulties are associated with authentication and processing such requests on the client.
The problem
Imagine, that you have some web-application which communicates with server by using jQuery and JSON.
Server side:
[HttpPost]
public ActionResult GetData()
{
return Json(new
{
Items = new[]
{
"Li Chen",
"Abdullah Khamir",
"Mark Schrenberg",
"Katy Sullivan",
"Erico Gantomaro",
}
});
}
Client side:
var $list = $("#list");
var $status = $("#status");
$list.empty();
$status.text("Loading...");
$.post("/home/getdata")
.always(function() {
$status.empty();
})
.success(function(data) {
for (var i = 0; i < data.Items.length; i++) {
$list.append($("<li/>").text(data.Items[i]));
}
});
That is pretty simple. Let’s add a simple authentication to our web-application. And again it is a pretty simple – just use Forms Authentication and Authorize attribute from ASP.NET MVC. The controller’ code will change as:
[HttpPost]
[Authorize]
public ActionResult GetData()
{
return Json(new
{
Items = new[]
{
"Li Chen",
"Abdullah Khamir",
"Mark Schrenberg",
"Katy Sullivan",
"Erico Gantomaro",
}
});
}
Still have no problems. Once user authenticated in application, he able to see the page and can retreive a data. However, there is a some problem when authentication timeout expired. In this case server will send to user…HTTP 302 Found.
Obviously, that the client code in our case did not expect this and as a result application will not work properly.
Reasons
Before we will fix the code, let’s understand what happens. Why server send HTTP 302? That would be logical to assume that there must be a HTTP 401. The point is that when we use FormsAuthentication at backstage acts FormsAuthenticationModule (which registered in global web.config file by default). Look under the hood of the module, you can easily understand that if the current code for HTTP is 401, then it performs a redirect, i.e. replaces it on 302:
It is easy to guess that the purpose is to redirect a user to the login page, if the request is not processed successfully (status code is 401). In this case, the user will see the good login form, but not the IIS error code. Makes sense, doesn't it?
From the point of view of our ASP.NET MVC application a pipeline is something like this:
- A request comes in to the application, where faced a filter AuthorizeAttribute.
- Because of the user is not authenticated, then this filter will return the HTTP 401, which is logical (see this easily enough by using reflector and check implementation of this filter).
- Well, after that our FormsAuthenticationModule acts and replaces HTTP 401 with redirect.
As a result – when we trying to request a page with usual HTTP-request we will see login page (which is good), but when we use AJAX-calls, there is too hard to parse such answers (which is bad).
The solution
So, what we need to solve the problem –
- Server should return HTTP 401/403 for AJAX-calls and HTTP 302 for usual HTTP-calls.
- To handle HTTP 401/403 on a client-side.
To be honest, you can override FormsAuthenticationModule logic to don’t replace HTTP 401 request with 302. To do it you can use SuppressFormsAuthenticationRedirect propery:
The only question is when to modify this property and who will do it?
Before answering this question, let’s turn our attention to how client should handle cases with HTTP-errors. There is two ways –
- The user is not authenticated in application (401). In this case we should redirect him to a login page.
- The user is authenticated in application, but have no enough permissions to access to the resource (403). For example, user not in role, which is necessary for current HTTP-request. In this case it is too silly to redirect him to a login page. The more preferred way is to say him that he is not have a necessary permissions.
Thus we should handle both cases. Let’s take a look on AuthorizeAttribute once again.
…i.e. he will always send HTTP 401.
Not good. Therefore we should fix a bit this behavior. So, let’s get started.
First – let’s determine whether current HTTP-request is AJAX-request. If yes, we should disable replacing HTTP 401 with HTTP 302:
public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var httpContext = filterContext.HttpContext;
var request = httpContext.Request;
var response = httpContext.Response;
if (request.IsAjaxRequest())
response.SuppressFormsAuthenticationRedirect = true;
base.HandleUnauthorizedRequest(filterContext);
}
}
Second – let’s add a condition:: if user authenticated, then we will send HTTP 403; and HTTP 401 otherwise.
public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var httpContext = filterContext.HttpContext;
var request = httpContext.Request;
var response = httpContext.Response;
var user = httpContext.User;
if (request.IsAjaxRequest())
{
if (user.Identity.IsAuthenticated == false)
response.StatusCode = (int)HttpStatusCode.Unauthorized;
else
response.StatusCode = (int)HttpStatusCode.Forbidden;
response.SuppressFormsAuthenticationRedirect = true;
response.End();
}
base.HandleUnauthorizedRequest(filterContext);
}
}
Well done. Now we should replace all usings of standard AuthorizeAttribute with this new filter. It may be not applicable for sime guys, who is aesthete of code. But I don’t know any other way. If you have, let’s go to comments, please.
The last, what we should to do – to add HTTP 401/403 handling on a client-side. We can use ajaxError at jQuery to avoid code duplication:
$(document).ajaxError(function (e, xhr) {
if (xhr.status == 401)
window.location = "/Account/Login";
else if (xhr.status == 403)
alert("You have no enough permissions to request this resource.");
});
The result –
- If user is not authenticated, then he will be redirected to a login page after any AJAX-call.
- If user is authenticated, but have no enough permissions, then he will see user-friendly erorr message.
- If user is authenticated and have enough permissions, the there is no any errors and HTTP-request will be proceeded as ususal.
The only disadvantage is necessity to use ApplicationAuthorizeAttribute instead of standard AuthorizeAttribute. So, if you have some code which uses AuthorizeAttribute, then you should fix it.
Sources placed on github.