For modern web applications, it has become the norm to use AJAX when creating user interfaces. However, because of this, sometimes, additional difficulties arise. Often these difficulties are associated with authentication and the processing of such requests on the client.
Problem
Suppose that our web application, using jQuery, accesses the server and receives data from there in the form of JSON.
Server:
')
[HttpPost]
public ActionResult GetData ()
{
return Json (new
{
Items = new []
{
"Li Chen",
"Abdullah Khamir",
"Mark Schrenberg",
"Katy Sullivan",
"Erico Gantomaro",
}
});
}
Customer:
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]));
}
});
The logic is extremely simple and straightforward. Now we add authentication mechanisms to our application. With this, again, everything is quite simple - we use the good old FormsAuthentication mechanism and the Authorize attribute in ASP.NET MVC. The code of our controller will change as follows:
[HttpPost]
[Authorize]
public ActionResult GetData ()
{
return Json (new
{
Items = new []
{
"Li Chen",
"Abdullah Khamir",
"Mark Schrenberg",
"Katy Sullivan",
"Erico Gantomaro",
}
});
}
Again - there are no problems with this code. After authentication, the user sees the page and can receive data as before. However, the problem is that if the authentication time expires (due to inactivity of the user) or the user logs out (for example, in another browser tab), the server will respond to the controller in response to a call to the action ...
HTTP 302 Found .
It is obvious that the client code in our case did not expect such a turn of events and the application will continue to work incorrectly.
The reasons
Before solving a problem, let's understand the reasons - where did
HTTP 302 come from? It would seem - and here 302? It would be
logical to assume that there should be HTTP 401. The fact is that when we use FormsAuthentication behind the scenes, the FormsAuthenticationModule (which is added to the list of HTTP modules in the global configuration file) is in effect. Looking under the hood of this module, you can easily understand that if the current code for HTTP is 401, then it performs a redirect, i.e. replaces it with 302:
It is not hard to guess that this was done with the aim of redirecting the user to the password entry page if the request was not processed successfully and you need to specify a password (return code is 401). In this case, the user will see a welcome password entry form, rather than an IIS page with an error code. Does it make sense, doesn’t it?
From the point of view of our ASP.NET MVC application, the chain is as follows:
- The request comes to the application where it bumps into the AuthorizeAttribute filter.
- Since the user is not authenticated, this filter will give HTTP 401, which is logical (it is easy to verify this by looking at the implementation of this filter with a reflector).
- Well, then our FormsAuthenticationModule works, which replaces 401 with a redirect.
As a result, when accessing a password-protected page, we see a password entry form (
which is good ), but when accessing similar resources via AJAX, this answer is not informative (
which is bad ).
Decision
So, what is needed to solve the problem -
- That the server gave 401/403 for AJAX requests, and 302 for normal requests.
- Handle on client 401/403.
To be honest, FormsAuthenticationModule can be made not to replace requests from 401. For this, HttpResponse has a special property -
SuppressFormsAuthenticationRedirect :
The only question is in what cases to change this property and, last but not least, who exactly will do it?
Before answering this question, let's pay attention to how the client should react when it receives an HTTP response with an error in response to an AJAX request. There can be two scenarios:
- The user is not authenticated at all in the system ( 401 ) and needs to be sent to the page with the login of the password.
- The user is authenticated, but this action is still not available to him ( 403 ). For example, we can allow some action for certain roles in which the user is not included. Then sending it to the password entry page is probably silly - in this case it will be enough just to inform him that he does not have enough authority.
Thus, we need to handle two situations in different ways. Take a look at the AuthorizeAttribute again with a reflector.
…those. he always returns 401.
Not good. Therefore, it is necessary to slightly correct the standard behavior. So, let's begin.
The first is to determine whether the current request is an AJAX request and, if so, turn off the redirect to the password entry page:
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 , add the condition: if the user is authenticated, then we give 403, if not, then 401:
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);
}
}
New filter is ready. Now we should use the filter we just created in our application, instead of the standard AuthorizeAttribute. To aesthetes this may seem like a huge disadvantage, but I don’t see any other way here. If there is a solution, I will be glad to see it in the comments.
Well, the last thing to do is to add 401/403 processing on the client. In order not to do this for each request, you can use the ajaxError handler in jQuery:
$ (document) .ajaxError (function (e, xhr) {
if (xhr.status == 401)
window.location = "/ Account / Login";
else if (xhr.status == 403)
alert ("You have this resource.");
});
Eventually -
- If the user is not authenticated (the timeout has expired, for example), then after any AJAX request, it will be sent to the password entry page.
- If the user is authenticated, but does not have the authority to perform the action, he will see the corresponding error message.
- If the user is authenticated and right enough, the action will be executed.
Of the minuses - you must use the new filter ApplicationAuthorizeAttribute, instead of the standard. Accordingly, if the standard code has already been used, then this code will have to be changed in all places.
The source code for the application is
on github .