Saturday, July 24, 2004

Mixing Forms and Windows Authentication

A client of mine wants to provide single sign-on (SSO) capabilities in their web application, so that users don't have to type in their domain password when authenticating to the application. The twist? Only some users of the application use SSO: the rest have accounts that exist only in the application database. So we couldn't just flip on “integrated authentication” in IIS and party on. But with the help of Keith Brown, I was able to figure out a pretty nifty solution.


The trick was realizing that if you enable both “anonymous“ and “integrated“ authentication for a particular virtual directory, the browser won't try to authenticate to the web server until it receives a 401 (Unauthorized) back from the web server. But you can issue your own 401 any time you like! So what I did was to just set up Forms authentication as normal, but also provided a checkbox on the login form that said, “Use my network credentials.” Then, in my login form, I did something like this:


public class Login : Page {
  protected
Label ErrorMessageLabel;
  protected
TextBox UsernameTextBox;
  protected
TextBox PasswordTextBox;
  protected
CheckBox CheckBox1;

  public void Page_Load(object
o, EventArgs e) {
    if
(IsPostBack) {
      string authenticatedUser = null
;
      if (CheckBox1.Checked)
// Use their network credentials
     
{
        string
user = Request.ServerVariables["LOGON_USER"];
        if (user.Length == 0)
// They haven't provided credentials yet
       
{
         
Response.StatusCode = 401;
          Response.StatusDescription = "Unauthorized";
          Response.End();
        }
        else
// They have
       
{
         
authenticatedUser = user;
       
}
      }
      else
// Use the username and password they provide
     
{
         if
(IsPasswordOK(UsernameTextBox.Text, PasswordTextBox.Text)) {
          authenticatedUser = UsernameTextBox.Text;
        }
      }
      if (authenticatedUser != null)
// They authenticated successfully
     
{
        // Issue the Forms Auth cookie and send them on their way

        FormsAuthentication.RedirectFromLoginPage(authenticatedUser, false
);
      }
      else
// They didn't
     
{
       
ErrorMessageLabel.Text = "Invalid username or bad password. Please try again.";
     
}
    }
  }
}


What this does is - when the user submits the login - check to see whether they want to authenticate by providing a username and password (normal Forms authentication) or whether they want to authenticate automatically, using their logged-in credentials. Right now, I'm figuring this out by having them explicity check a checkbox, but I do lots of other things. For example, I could have them always enter their username, and then go look in the database to see whether they're supposed to get a SSO login or a normal one. Or I could have them check the checkbox once and remember their settings forever after in a cookie.


Whatever mechanism I decide on, the trick here is that I can force the browser to authenticate by sending back a 401. Then, in subsequent visits, I can check the LOGON_USER server variable to see if the authentication was successful or not. If it is, I'm perfectly welcome to issue them a valid Forms Authentication login, secure in the knowledge that the user has proven knowledge of their password to IIS already.


If the user is using IE, the authentication will happen automatically, using whatever credentials they're logged in to the client machine with. It works in Firefox, too, but they get that little username/password popup dialog box. Oh well - maybe the Firefox people will add auto login in a future release, or someone will write an extension. But failing that, providing SSO only to IE users is good enough for us.

27 comments:

  1. Mixing Forms and Windows Authentication

    ReplyDelete
  2. That, sir, is completely hardcore. :) Slick.

    ReplyDelete
  3. I aim to please. ;)

    ReplyDelete
  4. I'm not sure I understand. This is an IE-only solution, and IE already doesn't pop up a dialog box.



    In order to never display a login form, you have to catch the second 401 that happens when they provide network credentials that are no good, or fail to provide any to your first challenge. And the problem with that is, the dialog box that pops up for non-IE users pops up after the first challenge, so you'd still have that.



    I guess what you'd have to do is to figure out based on the user-agent string which browser they're using, and react accordingly. Assuming they haven't used one of the many available plugins to make it lie to you. :) If you did that, though, you'd be able to skip displaying the form and go straight to the challenge.



    All in all, it would be a moderately complicated piece of code.

    ReplyDelete
  5. IE doesn't always let you slip through withouth the dialog box. For example, if you are accessing the site that is not in your Local Intranet zone, you will get the dialog box no matter what. Credentials Screening solution I proposed addresses this exact problem. Otherwise, in your solution, even after you check the "use network credentials" checkbox, you will a standard security dialog box unless the site in Local Intranet zone, which may or may not be the case, depending on the configuration of the network.



    I agree, it adds complexity, but it also adds completeness :)

    ReplyDelete
  6. Interesting! I didn't know that about the Intranet Zone!



    Thanks for explaining. :)

    ReplyDelete
  7. Not a problem. Now I can go tell people for a week that I taught Craig Andera something :)

    ReplyDelete
  8. Heh. I think you need some new goals: finding something I don't know is too easy. :)

    ReplyDelete
  9. Interestingly enough, I just read this blog: http://ackbarr.xoops.org/archives/2005/03/31/integrated-windows-authentication-in-firefox/

    Getting Windows Authentication working in Firefox

    If you have control of the browser settings (it's in user.js in Firefox) you can make it a default setting.

    This is a pretty old blog so this may not be relevant info anymore.

    Just trying to help out.

    ReplyDelete
  10. Interesting - I'll have to check that out. I don't think it helps my client, but it might help *me*, which is more important. :)



    Thanks!

    ReplyDelete
  11. Interesante tip de como proveer un proceso de autentificación integrada tanto para usuarios que puedes...

    ReplyDelete
  12. Brilliant work! Thanks for posting this, Craig. It was exactly what I was looking for.

    ReplyDelete
  13. Craig or Dimitri,

    Do you still have the code for the credentialing screening available? Looks like the link above no longer exists and I would like to look closer at Dimitri's code.



    Thanks in advance.

    Doug

    ReplyDelete
  14. I'm trying to use the above (which is awesome) and avoid the login pop-up but rather gather the user context since they have already logged into the domain.

    ReplyDelete
  15. Which link are you talking about that no longer exists?

    ReplyDelete
  16. How can i access to the Default page directly when i am in INTRANET.I'm tryung to eliminate the check box:
    INTRANET ----> Page Default
    INTERNET ------> login page ---OK----> Page Default

    ReplyDelete
  17. You could try an approach like this one:

    http://glazkov.com/2004/06/06/credentials-screening/

    ReplyDelete
  18. yes it's true,BUT
    For internet Access i want to have my personel login page where i test the validity of username and login by my self.that's mean
    Internet ----> "MY login Forms page and not Windows login" ----function IsPasswordOK(string,string) return true--> Page Default.
    I think that IIS have some impact to do it.

    Thank you craig-andera!

    ReplyDelete
  19. Looks like the xmlhttprequest object supports username and password authentication. So write some javascript that attempts to retrieve a page with your username and password. If it works, the username and password are authentic.

    ReplyDelete
  20. :)
    think you craig
    but exactly i need a solution for this constraint:
    if the user is from my local network and Domain he should access to the site (Default page) directly.
    if the user is from internet or another Domain he access to my login.aspx page and he write user/password ;than i have a test throw an SQL table if the user/password are valid so access to Default.aspx.

    ReplyDelete
  21. Unfortunately, the main promise of Credentials-Screening article (http://glazkov.com/2004/06/06/credentials-screening/) article doesn’t work: a browser still displays that automatic pop-up asking for Windows credentials, in case user is not authenticated [yet]. That remote trick with ActiveXObject(”Msxml2.DOMDocument”) (and I tried XmlHttpRequest too – obviously, no difference) does not allow a browser to silently fail…

    I’m currently using IIS7, but may be able to try it with IIS5.1. I doubt it matters, though.

    Did anyone actually have it working?

    ReplyDelete
  22. did someone make this work ?

    ReplyDelete
  23. How do you get the roles the user is in? Request.ServerVariables["LOGON_USER"] gives you the user name but where are the roles?

    ReplyDelete
  24. You'll need to call back into the system to find what roles are associated with the user: the browser doesn't send that information along, which makes sense, since it could lie.

    What API you call to get this information depends on where the users are stored.

    ReplyDelete
  25. ToDimitri Glazkov
    Thanks for the intranet zone tip.. That did the trick for me after 4 hours of frustration.

    ReplyDelete
  26. I'm trying to implement this in MVC3 and not getting too far. I have a few questions. Does your user have to submit twice to logon? He tries to log on once with the checkbox checked, gets a 401 back. Then he has to submit again, and the previous 401 response causes his browser to send credentials? Or, does the Checkbox do a postback?

    ReplyDelete
  27. @Sean, the roles can be got via the LogonUserIdentity object:

    System.Security.Principal.WindowsIdentity wi = Request.LogonUserIdentity;

    Others:
    On a side note: I got it working in MVC3 (is integrated with my custom STS, so my STS converts the windows user into a formsidentity.)

    For Windows authentication, I redirect to a seperate page which has Forms/Anonymous Authentication disabled and Windows Authentication enabled. For other users (or applications aka Relying Parties in WIF terminology), I redirect to regular Forms Authentication page.

    This link which helped me a lot:
    http://mvolo.com/blogs/serverside/archive/2008/02/11/IIS-7.0-Two_2D00_Level-Authentication-with-Forms-Authentication-and-Windows-Authentication.aspx

    Good luck.

    ReplyDelete