简体   繁体   中英

Forms authentication: disable redirect to the login page

I have an application that uses ASP.NET Forms Authentication. For the most part, it's working great, but I'm trying to add support for a simple API via an .ashx file. I want the ashx file to have optional authentication (ie if you don't supply an Authentication header, then it just works anonymously). But, depending on what you do, I want to require authentication under certain conditions.

I thought it would be a simple matter of responding with status code 401 if the required authentication was not supplied, but it seems like the Forms Authentcation module is intercepting that and responding with a redirect to the login page instead. What I mean is, if my ProcessRequest method looks like this:

public void ProcessRequest(HttpContext context)
{
    Response.StatusCode = 401;
    Response.StatusDescription = "Authentication required";
}

Then instead of getting a 401 error code on the client, like I expect, I'm actually getting a 302 redirect to the login page.

For nornal HTTP traffic, I can see how that would be useful, but for my API page, I want the 401 to go through unmodified so that the client-side caller can respond to it programmatically instead.

Is there any way to do that?

ASP.NET 4.5 added the Boolean HttpResponse.SuppressFormsAuthenticationRedirect property.

public void ProcessRequest(HttpContext context)
{
    Response.StatusCode = 401;
    Response.StatusDescription = "Authentication required";
    Response.SuppressFormsAuthenticationRedirect = true;
}

After a bit of investigation, it looks like the FormsAuthenticationModule adds a handler for the HttpApplicationContext.EndRequest event. In it's handler, it checks for a 401 status code and basically does a Response.Redirect(loginUrl) instead. As far as I can tell, there's no way to override this behaviour if want to use FormsAuthenticationModule .

The way I ended up getting around it was by disabling the FormsAuthenticationModule in the web.config like so:

<authentication mode="None" />

And then implementing the Application_AuthenticateEvent myself:

void Application_AuthenticateRequest(object sender, EventArgs e)
{
    if (Context.User == null)
    {
        var oldTicket = ExtractTicketFromCookie(Context, FormsAuthentication.FormsCookieName);
        if (oldTicket != null && !oldTicket.Expired)
        {
            var ticket = oldTicket;
            if (FormsAuthentication.SlidingExpiration)
            {
                ticket = FormsAuthentication.RenewTicketIfOld(oldTicket);
                if (ticket == null)
                    return;
            }

            Context.User = new GenericPrincipal(new FormsIdentity(ticket), new string[0]);
            if (ticket != oldTicket)
            {
                // update the cookie since we've refreshed the ticket
                string cookieValue = FormsAuthentication.Encrypt(ticket);
                var cookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName] ??
                             new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue) { Path = ticket.CookiePath };

                if (ticket.IsPersistent)
                    cookie.Expires = ticket.Expiration;
                cookie.Value = cookieValue;
                cookie.Secure = FormsAuthentication.RequireSSL;
                cookie.HttpOnly = true;
                if (FormsAuthentication.CookieDomain != null)
                    cookie.Domain = FormsAuthentication.CookieDomain;
                Context.Response.Cookies.Remove(cookie.Name);
                Context.Response.Cookies.Add(cookie);
            }
        }
    }
}

private static FormsAuthenticationTicket ExtractTicketFromCookie(HttpContext context, string name)
{
    FormsAuthenticationTicket ticket = null;
    string encryptedTicket = null;

    var cookie = context.Request.Cookies[name];
    if (cookie != null)
    {
        encryptedTicket = cookie.Value;
    }

    if (!string.IsNullOrEmpty(encryptedTicket))
    {
        try
        {
            ticket = FormsAuthentication.Decrypt(encryptedTicket);
        }
        catch
        {
            context.Request.Cookies.Remove(name);
        }

        if (ticket != null && !ticket.Expired)
        {
            return ticket;
        }

        // if the ticket is expired then remove it
        context.Request.Cookies.Remove(name);
        return null;
    }
}

It's actually slightly more complicated than that, but I basically got the code by looking at the implementation of FormsAuthenticationModule in reflector. My implementation is different to the built-in FormsAuthenticationModule in that it doesn't do anything if you respond with a 401 - no redirecting to the login page at all. I guess if that ever becomes a requirement, I can put an item in the context to disable the auto-redirect or something.

I'm not sure if this will work for everyone, but in IIS7 you can call Response.End() after you've set the status code and description. This way, that #&$^#@*! FormsAuthenticationModule won't do a redirect.

public void ProcessRequest(HttpContext context) {
    Response.StatusCode = 401;
    Response.StatusDescription = "Authentication required";
    Response.End();
}

To build on zacharydl's answer slightly, I used this to solve my woes. On every request, at the beginning, if it's AJAX, immediately suppress the behavior.

protected void Application_BeginRequest()
{
    HttpRequestBase request = new HttpRequestWrapper(Context.Request);
    if (request.IsAjaxRequest())
    {
        Context.Response.SuppressFormsAuthenticationRedirect = true;
    }
}

I don't know how that Response.End() worked for you. I tried it with no joy, then looked at MSDN for Response.End(): 'stops execution of the page, and raises the EndRequest event'.

For what it's worth my hack was:

_response.StatusCode = 401;
_context.Items["401Override"] = true;
_response.End();

Then in Global.cs add an EndRequest handler (which will get called after Authentication HTTPModule):

protected void Application_EndRequest(object sender, EventArgs e)
{
    if (HttpContext.Current.Items["401Override"] != null)
    {
        HttpContext.Current.Response.Clear();
        HttpContext.Current.Response.StatusCode = 401;
    }
}

what you've found out is correct about the forms auth intercepting the 401 and doing a redirect but we also can do that to reverse that.

Basically what you need is an http module to intercept the 302 redirect to the login page and reverse it to a 401.

Steps on doing that is explained in here

The given link is about a WCF service but it is the same in all the forms auth scenarios.

As explained in the above link you need to clear the http headers as well but remember to put the cookie header back to the response if the original response (ie before intercepting) contained any cookies.

I know there is already an answer with tick but while trying to solve similar problem I found this ( http://blog.inedo.com/2010/10/12/http-418-im-a-teapot-finally-a-%e2%80%9clegitimate%e2%80%9d-use/ ) as an alternative.

Basically you return your own HTTP status code (eg 418) in your code. In my case a WCF data service.

throw new DataServiceException(418, "401 Unauthorized");

Then use a HTTP module to handle it at the EndRequest event to rewrite the code back to 401.

HttpApplication app = (HttpApplication)sender;
if (app.Context.Response.StatusCode == 418)
{
    app.Context.Response.StatusCode = 401;
}

The browser / client will receive the correct content and status code, it works great for me :)

If you are interested to learn more about HTTP status code 418 see this question & answer .

这是一个已知问题,并且有一个NuGet 包和/或可用的源代码

Funny hack if you use.NET Framework >= v4.0 but < v4.5. It uses reflection to set value of inaccessible SuppressFormsAuthenticationRedirect property:

// Set property to "true" using reflection
Response
  .GetType()
  .GetProperty("SuppressFormsAuthenticationRedirect")
  .SetValue(Response, true, null);

You do not set the WWW-Authenticate header in the code you show, so the client cannot do HTTP authentication instead of forms authentication. If this is the case, you should use 403 instead of 401, which will not be intercepted by the FormsAuthenticaitonModule .

I had the problem that I wanted to avoid not only the redirect but also the forms authentication itself in order to make a web api work. Entries in web.config with a location tag for the api didn't help. Thus I used SuppressFormAuthenticationRedirect and HttpContext.Current.SkipAuthorization to suppress the authentication in general. In order to identify the sender I used eg the UserAgent in the Header but it is of course recommendable to implement further authentification steps, eg check against the sending IP or send another key with the request. Below is inserted in the Global.asax.cs.

protected void Application_BeginRequest(object sender, EventArgs e)
    {
        if (HttpContext.Current.Request.UserAgent == "SECRET-AGENT")
        {
            AppLog.Log("Redirect suppressed");

            HttpApplication context = (HttpApplication)sender;

            context.Response.SuppressFormsAuthenticationRedirect = true;
            HttpContext.Current.SkipAuthorization = true;                
        }
    }

In order to redirect the user to Unauthorize Page rather than to the login page, the Hack is to implement Application_EndRequest in Global and check for Response Status Code 302, which is a temporary redirect from the current called to action.

protected void Application_EndRequest(object sender, EventArgs e)
    {
        if(HttpContext.Current.Response.StatusCode == 302 && User.Identity.IsAuthenticated)
        {
            HttpContext.Current.Response.Clear();
            HttpContext.Current.Response.Redirect("/UnauthorizedPageUrl");
        }
    }

Look inside your Web.config file in configuration\\authentication . If there is a forms subelement there with a loginUrl attribute, remove it and try again.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM