Monday, June 19, 2006

Mixing Forms and Windows Authentication in ASP.NET 2.0

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.

18 comments:

  1. AFAIK, you can't just hook into the ApplicationInstance events inside a page or other regular object. Only the Global.asa and custom HttpModules can do it. I ran into the same problem when I was trying to hook the EndRequest to clean up my NHibernate session I was keeping in the HttpContext.Current.Items collection. I ended up making an HttpModule to do it.

    ReplyDelete
  2. OK, thanks. Another friend told me the same thing. It's too bad - it would be pretty handy to be able to dynamically attach events. Of course, you'd have to be careful not to leak memory like crazy. :)

    ReplyDelete
  3. Interesing. Scott and I recently published an MSDN article along something similar, except we addressed the issue of running standard HTTP authentication (Basic/Digest) alongside Forms authentication on the same web application. You can find the article here:



    http://msdn.microsoft.com/library/en-us/dnaspp/html/MADAM.asp?frame=true



    The supplied solution, called MADAM (Mixed Authentication Disposition ASP.NET Module), solves the problem in a similar fashion. That is, an HttpModule is installed that conditionally sets the status code to 401 so that the browser would pop its credentials dialog. The *condition* is entirely configurable as opposed to being hard-coded, so you can teach the puppy new tricks we didn't think of. :) I've done a mental walkthrough, so in theory, I can imagine how the supplied HttpModule (FormsAuthenticationDispositionModule) could help in your case as well.

    ReplyDelete
  4. Hi Craig,



    Do you have any sample code in which you have implemented mixed mode authentication.

    Regards

    -Tej

    tej_pratap@countrywide.com

    ReplyDelete
  5. The code I posted is pretty much everything you need. What else are you looking for?

    ReplyDelete
  6. Craig, I was looking for an ASP.NET 1.1 mixed authentication mechanism and I've got to say that was by far the easiest workaround i've seen. I know there might be issues with .NET 2.0 but don't need to worry about it for now. Great stuff! Thanks!

    ReplyDelete
  7. Glad you found it helpful! I wound up using something similar to this in FlexWiki 2.0 as well, so it's something that keeps coming back up.

    ReplyDelete
  8. Craig,



    Man... We just thought we were going crazy! We had EXACTLY the same issue and your blog on this REALLY helped! Many Thanks!

    ReplyDelete
  9. My pain is your gain. ;)

    ReplyDelete
  10. Could you please provide step process and sample code of mixed mode authentication in asp.net 2.0

    ReplyDelete
  11. What have you tried so far?

    ReplyDelete
  12. Hi there, did you get a fix to this problem? I have having the same problem and your help will be much appreciated

    ReplyDelete
  13. A fix to what problem? Not being able to attach that event?

    ReplyDelete
  14. Craig, thanks for this post. One question...where's the best spot to put Context.Items["Send401"] = true; ? Everything works so far with exception to the 401. I'm sorry for what may seem like a very basic question. This is something that's way beyond my expert level...and you've made it simple up to this point.

    Thanks again

    ReplyDelete
  15. I put mine in my login page (login.aspx.cs). I don't remember exactly where, but presumably it was somewhere inside the if branch where I determined that Windows Authentication was needed.

    ReplyDelete
  16. I had a friend test it from the web and they're prompted for a user name and password. I'm assuming that the desired result is to display the 401 error without the challenge?

    ReplyDelete
  17. The only way you can get it not to challenge you is if it thinks you're on the *intra*net (not the Internet). And even then I think it might only be IE that does that, not Firefox, although I could be wrong.

    Under IE, the test for the intranet zone is simple: if the server name does not contain a dot (e.g. foobar versus www.foobar.com), then you're in the intranet zone.

    So generally, it's not possible to get the "auto-login" effect from the Internet. Unless someone else has something more clever than what I came up with figured out.

    ReplyDelete
  18. I too am facing a similar problem where I have a web that is both Intranet and Internet facing. My thing is that I don't want Internet users being prompted for a IIS user name and password they don't posses, I was hoping to transparently redirect them to the application login page. Are there any other "solid" methods for detecting intranet and internet users in hopes of avoiding the popup login window?

    ReplyDelete