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 (i.e. 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?


Solution 1:

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;
}

Solution 2:

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.

Solution 3:

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();
}

Solution 4:

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;
    }
}

Solution 5:

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;
    }
}