Prevent Forms Authentication Login Page Redirect When You Don’t Want It

redirect Go that way instead - Photo by JacobEnos CC some rights reserved

In an ASP.NET web application, it’s very common to write some jQuery code that makes an HTTP request to some URL (a lightweight service) in order to retrieve some data. That URL might be handled by an ASP.NET MVC controller action, a Web API operation, or even an ASP.NET Web Page or Web Form. If it can return curly brackets, it can be respond to a JavaScript request for JSON.

One pain point when hosting lightweight HTTP services on ASP.NET is making a request to a URL that requires authentication. Let’s look at a snippet of jQuery to illustrate what I mean. The following code makes a request to /admin/secret/data. Let’s assume that URL points to an ASP.NET MVC action with the AuthorizeAttribute applied, which requires that the request must be authenticated.

$.ajax({
    url: '/admin/secret/data',
    type: 'POST',
    contentType: 'application/json; charset=utf-8',
    statusCode: {
        200: function (data) {
            alert('200: Authenticated');
            // Bind the JSON data to the UI
        },
        401: function (data) {
            alert('401: Unauthenticated');
            // Handle the 401 error here.
        }
    }
});

If the user is not logged in when this code executes, you would expect that the 401 status code function would get called. But if forms authentication (often called FormsAuth for short) is configured, that isn’t what actually happens. Instead, you get a 200  with the contents of the login page (or a 404 if you don’t have a login page). What gives?

If you crack open Fiddler, it’s easy to see the problem. Instead of the request returning an HTTP 401 Unauthorized status code, it instead returns a 302 pointing to a login page. This causes jQuery (well actually, the XmlHttpRequest object) to automatically follow the redirect and issue another request to the login page. The login page handles this new request and return its contents with a 200 status code. This is not the desired result as the code expects JSON data to be returned in response to a 200 code, not HTML for the login page.

This “helpful” behavior when requesting a URL that requires authentication is a consequence of having the FormsAuthenticationModule enabled, which is the default in most ASP.NET applications. Under the hood, the FormsAuthenticationModule hooks into the request pipeline and changes any request that returns a 401 status code into a redirect to the login page.

Possible Solutions

I’m going to cover a few possible solutions I’ve seen around the web and then present the one that I prefer. It’s not that these other solutions are wrong, but they are only correct in some cases.

Remove Forms Authentication

If you don’t need FormsAuth, one simple solution is to remove the forms authentication module as this post suggests. This is a great solution if you’re sole purpose is to use ASP.NET to host a Web API service and you don’t need forms authentication. But it’s not a great solution if your app is both a web application and a web service.

Register an HttpModule to convert Redirects to 401

This blog post suggests registering an HTTP Module that converts any 302 request to a 401. There are two problems with this approach. The first is that it breaks the case where the redirect is legitimate and not the result of FormsAuth. The second is that it requires manual configuration of an HttpModule.

Install-Package MembershipService.Mvc

My colleague, Steve Sanderson, has an even better approach with his MembershipService.Mvc and MembershipService.WebForms NuGet packages. These packages expose ASP.NET Membership as a service that you can call from multiple devices.

For example, if you want your Windows Phone application to use an ASP.NET website’s membership system to authenticate users of the application, you’d use his package. He provides the MembershipClient.WP7 and MembershipClient.JavaScript packages for writing clients that call into these services.

These packages deserve a blog post in their own right, but I’m going to just focus on the DoNotRedirectLoginModule he wrote. His module takes a similar approach to the previous one I mentioned, but he checks for a special value in HttpContext.Items, a dictionary for storing data related to the current request, before reverting a redirect back to a 401.

To prevent a FormsAuth redirect, an action method (or ASP.NET page or Web API operation) would simply call the helpful method DoNotRedirectToLoginModule.ApplyForRequest. This sets the special token in HttpContext.Items and the module will rewrite a 302 that’s redirecting to the login page back to a 401.

My Solution

Steve’s solution is a very good one. But I’m particularly lazy and didn’t want to have to call that method on every action when I’m writing an Ajax heavy application. So what I did was write a module that hooks in two events of the request.

The first event, PostReleaseRequestState, occurs after authentication, but before the FormsAuthenticationModule converts the status to a 302. In the event handler for this event, I check to see if the request is an Ajax request by checking that the X-Requested-With request header is “XMLHttpRequest”.

If so, I store away a token in the HttpContext.Items like Steve does. Then in the EndRequest event handler, I check for that token, just like Steve does. Inspired by Steve’s approach, I added a method to allow explicitly opting into this behavior, SuppressAuthenticationRedirect.

Here’s the code for this module. Warning: Consider this “proof-of-concept” code. I haven’t tested this thoroughly in a wide range of environments.

public class SuppressFormsAuthenticationRedirectModule : IHttpModule {
  private static readonly object SuppressAuthenticationKey = new Object();

  public static void SuppressAuthenticationRedirect(HttpContext context) {
    context.Items[SuppressAuthenticationKey] = true;
  }

  public static void SuppressAuthenticationRedirect(HttpContextBase context) {
    context.Items[SuppressAuthenticationKey] = true;
  }

  public void Init(HttpApplication context) {
    context.PostReleaseRequestState += OnPostReleaseRequestState;
    context.EndRequest += OnEndRequest;
  }

  private void OnPostReleaseRequestState(object source, EventArgs args) {
    var context = (HttpApplication)source;
    var response = context.Response;
    var request = context.Request;

    if (response.StatusCode == 401 && request.Headers["X-Requested-With"] == 
      "XMLHttpRequest") {
      SuppressAuthenticationRedirect(context.Context);
    }
  }

  private void OnEndRequest(object source, EventArgs args) {
    var context = (HttpApplication)source;
    var response = context.Response;

    if (context.Context.Items.Contains(SuppressAuthenticationKey)) {
      response.TrySkipIisCustomErrors = true;
      response.ClearContent();
      response.StatusCode = 401;
      response.RedirectLocation = null;
    }
  }

  public void Dispose() {
  }

  public static void Register() {
    DynamicModuleUtility.RegisterModule(
      typeof(SuppressFormsAuthenticationRedirectModule));
  }
}

There’s a package for that

Warning: The following is proof-of-concept code I’ve written. I haven’t tested it thoroughly in a production environment and I don’t provide any warranties or promises that it works and won’t kill your favorite pet. You’ve been warmed.

Naturally, I’ve written a NuGet package for this. Simply install the package and all Ajax requests that set that header (if you’re using jQuery, you’re all set) will not be redirected in the case of a 401.

Install-Package AspNetHaack

Note that the package adds a source code file in App_Start that wires up the http module that suppresses redirect. If you want to turn off this behavior temporarily, you can comment out that file and you’ll be back to the old behavior.

The source code for this is in Github as part of my broader CodeHaacks project.

Why don’t you just fix the FormsAuthenticationModule?

We realize this is a deficiency with the forms authentication module and we’re looking into hopefully fixing this for the next version of the Framework.

What others have said

Requesting Gravatar... nacho10f Oct 04, 2011 7:32 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Steve Sanderson's packages really do deserve their own blog post.. I have no idea how to use them :s.
Requesting Gravatar... Fujiy Oct 04, 2011 8:35 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Recently I needed something similar. When user don´t have permission, returns a 401, and show a custom Html(custom error don´t work for 401 errors)

Additionally I needed to personalize assigned Roles for Anonymous users too. I wrote a custom AuthorizeAttribute that do this 3 things

ASP.NET vNext could have this 3 features by default....

-Authorize attribute(or web.config declarative way for webforms) return a 401 error
-Custom errors that works with 401 errors(maybe when fixing the first bug, this will automatically fixed)
-Roles for anonymous users
Requesting Gravatar... Matt Honeycutt Oct 04, 2011 1:23 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Why not utilize the IsAjaxRequest extension method instead of checking headers directly? They are equivalent, but one is much more readable than the other...
Requesting Gravatar... Michel Grootjans Oct 04, 2011 3:12 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Wouldn't it make more sense to have this be handled by a different HttpModule based on the url's extension.

In Rails, if the URL has no extension, it's supposed to be a browser call, where you might respond with a redirect.
If the URL has a .json extension, you get a plain error, not a redirect.
Requesting Gravatar... Saeed Neamati Oct 04, 2011 3:56 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
We had the same problem. But what we did, was to hook to AuthenticateRequest (just like you did) and we also checked the request to see if it's ajax or not (again, just like what you did). But at this point, we simply returned a JSON like {location: 'http://www.domain.com/path-to-login-page'} and we simply ended response in that method with HTTP code 200. This way, jQuery still gets a JSON result. But if the result has a "location" property, we simply do a client-side redirect to login page. That's our way and it works like a charm.
Requesting Gravatar... Mike Oct 04, 2011 5:46 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Thanks for the work-around. Can you ask the ASP.NET team that owns this module to add a configuration attribute for web.config, because I think that would be much easier.
Requesting Gravatar... Goran Obradovic Oct 04, 2011 6:13 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Nice package. Would come handy as I would have used it if it existed about 2 moths ago, but I will use it next time :)
Anyway, I solved it without module then, but as I didn't wrote about it, here is how if there is anyone like this guy who cannot/wont use modules: stackoverflow.com/.../7628655#7628655
Requesting Gravatar... Andy McGoldrick Oct 04, 2011 6:36 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Great post.

Do you need to create a HTTP module for this?

As your MVC application implements System.Web.HttpApplication you could wire all this up on the application_start and add the event handlers for the events required in the application class.

This might not be as tidy or reusable but for a single web app its probably less scary for folk who want to avoid HTTP Handlers.
Requesting Gravatar... Joseph Daigle Oct 04, 2011 8:43 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
If you want a super-simple 5 line solution, just turn all AJAX requested 302 responses into 401s: https://gist.github.com/1264267

At the end of the day, though, we ended up writing our own forms authentication module.
Requesting Gravatar... Chris Marisic Oct 04, 2011 10:00 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
What was Microsoft thinking when they needed to create "TrySkipIisCustomErrors"? That is one of the most nonsense ideas ever. Oh you said return a 410 error, you couldn't have meant to do that, we're going to do something else for you. Wait you said for us to to try to skip our custom "helping" carry on then.
Requesting Gravatar... Michael Oct 04, 2011 11:05 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Great article.


P.S. The font used for comments when displayed in Chrome is super small. I have to zoom in to read them.
Requesting Gravatar... Nicholas Carey Oct 05, 2011 3:38 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Thanks for trying to fix this problem. Forms Authentication turning returning 302 instead of a 401 and turning any 401 into a 302 redirect has always been most vexing been a headache for years.

Why don’t you just fix the FormsAuthenticationModule?

We realize this is a deficiency with the forms authentication module and we’re looking into hopefully fixing this for the next version of the Framework.


Specification of the correct behaviour has been part of the HTTP standard for more than 15 years now. HTTP 1.1, RFC 2616 (and before that, HTTP 1.0, RFC 1945) specify the correct behavior in sections 10.4.2 and 9.4 respectively (the verbiage hasn't changed significantly):

10.4.2 401 Unauthorized The request requires user authentication. The response MUST include a WWW-Authenticate header field (section 14.47) containing a challenge applicable to the requested resource.

The client MAY repeat the request with a suitable Authorization header field (section 14.8). If the request already included Authorization credentials, then the 401 response indicates that authorization has been refused for those credentials.

If the 401 response contains the same challenge as the prior response, and the user agent has already attempted authentication at least once, then the user SHOULD be presented the entity that was given in the response, since that entity might include relevant diagnostic information. HTTP access authentication is explained in "HTTP Authentication: Basic and Digest Access Authentication" [43].
Requesting Gravatar... Srini Oct 08, 2011 2:26 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Great Post!
Requesting Gravatar... Jarrett Vance Oct 08, 2011 11:20 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
The last part of this post is hilarious:
Why don’t you just fix the FormsAuthenticationModule?

I look forward to this being fixed in 4.5... please!

Here is my ugly solution in Application_Error:


// read error page from config
string redirectUrl = null;
if (this.Context.IsCustomErrorEnabled)
{
var section = WebConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;
if (section != null) {
redirectUrl = section.DefaultRedirect;
if (section.Errors.Count > 0) {
CustomError item = section.Errors[statusCode.ToString()];
if (item != null) redirectUrl = item.Redirect;
}
}

// load error page
this.Context.Response.Clear();
this.Context.Response.StatusCode = statusCode;
this.Context.Response.TrySkipIisCustomErrors = true;

this.Context.ClearError();

// ** do not redirect when ajax **
if ((this.Context.Request["X-Requested-With"] == "XMLHttpRequest") ||
((this.Context.Request.Headers != null) && (this.Context.Request.Headers["X-Requested-With"] == "XMLHttpRequest")))
return;

// only redirect if there is a url
if (!string.IsNullOrEmpty(redirectUrl))
{
this.Context.Response.Redirect(redirectUrl);
}
}
Requesting Gravatar... Anas Ghanem Oct 08, 2011 5:22 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
What about calling the HttpContext.Current.SkipAuthorization ?

msdn.microsoft.com/...
Requesting Gravatar... haacked Oct 10, 2011 7:25 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
@Anas, that skips authorization. If you didn't want authorization to happen on that method, just remove the AuthorizeAttribute. The point is that you really *do* want authorization.
Requesting Gravatar... Konstantin Oct 12, 2011 5:32 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
This solution didn't work for me, since UrlAuthorizationModule just ended the request, so PostReleaseRequestState event was not raised.
Requesting Gravatar... Softlion Oct 22, 2011 5:48 PM
# My solution
I wrote an AjaxAwareAuthorizeAttribute, inherited from AuthorizeAttribute, which returns an http 409 response (Switch Proxy) instead of a 401.
It sets the X-Redirect header to the intended redirect url and jQuery handles this globally as demonstrated in this blog post.

Ok you need to know which action is called by ajax, and set this filter specifically on those. My first approach was less intrusive as you leave the [Authorize] attribute but does not work as explained below.

My first idea : intercept action responses by setting once a global MVC action filter, which detects the result type and replace it with an AjaxRedirectResonse which returns 409 as above.
But MVC has a special handling of authorize filters : after MVC calls an authorize filter, if this filter changes the action result then MVC will bypasse all other filters, whichever type they have.
And [Authorize] is an authorize filter ... So this approach does not work and i sticked with the above version.


Bonus 1:
I wrote an AjaxAwareRedirectResult result type, which will
X-Redirect but also X-Redirect-Top. This handles the case where you want to redirect outside an iframe.

Bonus 2:
In AjaxAwareAuthorizeAttribute I modified the way FormsAuthentication gets the redirect url because the web.config version is not aware of areas and i needed one different auth page per area. But this is another story.
Requesting Gravatar... Jarrett Vance Dec 08, 2011 11:06 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Just realized I wrote a module using same method 3 years ago. See code here
blogsvc.codeplex.com/.../46874#397822
Requesting Gravatar... Sheron Benedict Jan 09, 2012 12:11 PM
# Does not work with WCF REST
This does not seem to work with WCF REST. When I debugged the HTTP module, I could see that the PostReleaseRequestState event is not being triggered at all. So when the EndRequest event handler executes, it does not find the token in the context. And hence the HTTP status code in the response remains as a 302 redirect.

In the end, I decided on a approach where I check in EndRequest if the redirect is to the login page. If so, I override it and return 401. Like this:



if (context.Response.IsRequestBeingRedirected && context.Response.RedirectLocation.Contains(FormsAuthentication.LoginUrl))
{
response.TrySkipIisCustomErrors = true;
response.ClearContent();
response.StatusCode = (int)HttpStatusCode.Unauthorized;
response.RedirectLocation = null;
}

Requesting Gravatar... Nirvan B Jan 18, 2012 2:14 PM
# re: An issue that cropped up after I went live with my application
I had successfully implemented the above IHttpModule in my web application and tested it on IIS 7.5 which I use for staging on my computer. It all worked well and so I deployed my web application on live hosting server. But, somehow on live server, the unauthenticated ajax-requests, forced browser to trigger windows/basic/digest authentication on the browser. Since I was using only Forms authentication in my web app, I was confused as to why the browser presented such "authentication required" popups, before running the ajax error handler (which redirects to forms login page). Later, I noticed that IIS adds "WWW-Authenticate" headers for a 401 request, so such 401 responses trigger the "Authentication Required" popup in browser before even running the ajax error handler for the ajax request. The only solution seems to me is to disable the Basic, Digest and Windows Authentication at web application level in IIS. This problem did not occurred on my staging server as Basic, Digest and Windows authentication modes were disabled for the web application.

Any better suggestions to tackle the problem that I am facing ?

regards,
Nirvan
Requesting Gravatar... Martin Odhelius Jan 19, 2012 9:00 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
Your first request shall probably result in a 401 that redirects to the login page, but then if the user is not able to authorize himself you shall probably consider to through HTTP error code 403.

"403 Forbidden

The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated. If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity. If the server does not wish to make this information available to the client, the status code 404 (Not Found) can be used instead. "

http://www.ietf.org/rfc/rfc2616.txt

Best Regards
Martin
Requesting Gravatar... Adrian Jan 22, 2012 1:48 AM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
You can also use MADAM (http://www.raboof.com/projects/madam/), which is similar to the module you describe. Check my answer on SO for more details.

http://stackoverflow.com/a/8950763/1161893
Requesting Gravatar... Chin Bae Jan 28, 2012 3:48 PM
# re: Prevent Forms Authentication Login Page Redirect When You Don’t Want It
I found that that PostReleaseRequestState event isn't firing in my WCF Web Api application either. I was thinking, why bother doing all this opt-in nonsense with the HttpContext.Items dictionary?

Can't you just check if the Response.StatusCode == 302 and Request.Headers["X-Requested-With"] is anything other than null and then change the StatusCode accordingly?

Are there any scenarios in which you'd want a 302 to go through other than when Request.Headers["X-Requested-With"] is null (i.e. through a regular browser request)?

The only question would be how reliable is Request.Headers["X-Requested-With"] as a means of determining the source of the request?

Are there any scenarios in which a regular browser request wouldn't have Request.Headers["X-Requested-With"] == null?

Are there any scenarios in which a non-browser requests would also cause Request.Headers["X-Requested-With"] == null?

For example, what's the Request.Headers["X-Requested-With"] value when making a request with the WebClient class or the HttpWebRequest class?

What do you have to say?

(will show your gravatar)
Please add 4 and 6 and type the answer here: