AJAX and FormsAuthentication, how prevent FormsAut

2020-05-15 14:45发布

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.

3条回答
神经病院院长
2楼-- · 2020-05-15 15:32

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.

查看更多
Luminary・发光体
3楼-- · 2020-05-15 15:34

I was having issues implementing the accepted answer. Chiefly, my error logs were getting filled with Server cannot set status after HTTP headers have been sent errors.

I tried implementing the accepted answer to question Server cannot set status after HTTP headers have been sent IIS7.5, again no success.

Googling a bit I stumbled upon the SuppressFormsAuthenticationRedirect property

If your .Net version is >= 4.5, then you can add the following code to the HandleUnauthorizedRequest method of your custom AuthorizeAttribute class.

public sealed class CustomAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
            base.HandleUnauthorizedRequest(filterContext);
            return;
        }

        base.HandleUnauthorizedRequest(filterContext);
        return;
    }
}

The important part is the if block. This is the simplest thing to do if you are on .Net 4.5 & already have custom authorization in place.

查看更多
我命由我不由天
4楼-- · 2020-05-15 15:40

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.

查看更多
登录 后发表回答