Almost two years ago, I blogged about how to mix Forms and Windows authentication in an ASP.NET application. It was something I figured out for a client, and they've been using the basic idea ever since. It worked pretty well up until a month or two ago. Then it broke pretty hard. What changed? We upgraded to ASP.NET 2.0.
Well, I finally got a chance to sit down and chase the problem, and I figured out what looks like a solution. We have yet to integrate my idea into the product, but I wrote a quick prototype, and it looks like it's going to work, so I thought I would blog it here.
The kernel of the problem is that the ASP.NET 2.0 Forms Authentication Module appears to stomp on the "Response.StatusCode = 401" that you need to set in order to make mixed authentication work. I'm not sure exactly where this happens: I got lost in Reflector when I tried to chase it down. But you can easily observe the effect - your login page will redirect back onto itself, with the ReturnUrl query string parameter double-escaped. That is, you'll see something like this in your address bar:
instead of this
Notice how the ReturnUrl is actually pointing back to the login page itself, rather than to Default.aspx the way it should be. Again, I haven't chased down exactly where this behavior is coming from, but it's easy enough to observe.
My solution was simply to attach a handler to the Application's EndRequest event by putting the following in Global.asax:
protected void Application_EndRequest(object sender, EventArgs e)
if (Context.Items["Send401"] != null)
Response.StatusCode = 401;
Response.StatusDescription = "Unauthorized";
Then, in order to trigger this code, all I have to do is put a
Context.Items["Send401"] = true;
somewhere in my login page when I determine that I need redirection to pick up on the user's Windows credentials. Because Application_EndRequest runs much later in the page lifecycle than the code in my login page, Forms Authentication can't get in the way and screw up my status change.
I thought I'd try to be really clever and attach my handler in the page itself by doing something like this:
Context.ApplicationInstance.EndRequest += LoginPage_EndRequest;
but it doesn't work. I'm not sure why (the page lifecycle is not my strong suit these days), and I'm out of time to chase it down. If you happen to know what I'm doing wrong, drop a comment. In the meantime, the Global.asax thing works well enough for us.