Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AJAX and FormsAuthentication, how prevent FormsAuthentication overrides HTTP 401?

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.

like image 711
vtortola Avatar asked Sep 23 '11 16:09

vtortola


2 Answers

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.

like image 122
Goran Obradovic Avatar answered Sep 25 '22 22:09

Goran Obradovic


I'm stealing this answer heavily from other posts, but an idea might be to implement an HttpModule to intercept the redirect to the login page (instructions at that link).

You could also modify that example HttpModule to only intercept the redirect if the request was made via AJAX if the default behavior is correct when the request is not made via AJAX:

Detect ajax call, ASP.net

So something along the lines of:

class AuthRedirectHandler : IHttpModule {     #region IHttpModule Members      public void Dispose()     {      }      public void Init(HttpApplication context)     {         context.EndRequest+= new EventHandler(context_EndRequest);     }       void context_EndRequest(object sender, EventArgs e)     {         HttpApplication app = (HttpApplication)sender;         if (app.Response.StatusCode == 302              && app.Request.Headers["X-Requested-With"] == "XMLHttpRequest"             && context.Response.RedirectLocation.ToUpper().Contains("LOGIN.ASPX"))         {             app.Response.ClearHeaders();             app.Response.ClearContent();             app.Response.StatusCode = 401;         }     }      #endregion } 

You could also ensure the redirect is to your actual login page if there are other legit 302 redirects in your app.

Then you would just add to your web.config:

  <httpModules>     <add name="AuthRedirectHandler" type="SomeNameSpace.AuthRedirectHandler, SomeNameSpace" />   </httpModules> 

Anyhow. Again, actual original thought went into this answer, I'm just pulling various bits together from SO and other parts of the web.

like image 34
Kevin Stricker Avatar answered Sep 23 '22 22:09

Kevin Stricker