Search code examples
c#asp.netforms-authenticationwindows-authentication

Suppress NTLM Authentication Dialog


Code

I have created a login page that combines Forms Authentication with Integrated Windows Authentication.

public partial class Login : System.Web.UI.Page
        {
        // http://www.innovation.ch/personal/ronald/ntlm.html
        // http://curl.cofman.dk/rfc/ntlm.html
        // http://blogs.msdn.com/b/chiranth/archive/2013/09/21/ntlm-want-to-know-how-it-works.aspx
        protected void Page_Load(object sender, EventArgs e)
            {
            if (!IsPostBack)
                {
                if (Request.Headers["Authorization"].IsNullOrEmpty())
                    {
                    Response.StatusCode = 401;
                    Response.AddHeader("WWW-Authenticate", "NTLM");
                    Email.SendMailToDebugger("Auth", "No Auth");
                    //Response.End();
                    }
                else if (Request.Headers["Authorization"].StartsWith("Negotiate"))
                    {
                    Response.StatusCode = 401;
                    Response.AddHeader("WWW-Authenticate", "NTLM");
                    Email.SendMailToDebugger("Auth", "Negotiate Auth");
                    Response.End();
                    }
                else if (Request.Headers["Authorization"].StartsWith("NTLM"))
                    {
                    string base64text = Request.Headers["Authorization"].Remove(0, 5); //Remove NTLM<space>
                    byte[] bytes = Convert.FromBase64String(base64text);
                    byte typebyte = bytes[8];

                    if (typebyte.ToString("X2") == "01") //type 1 message received
                        {
                        //send type 2 message
                        List<byte> responsebytes = new List<byte> { 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 00, 0x02, 0x02, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
                        string type2message = Convert.ToBase64String(responsebytes.ToArray());
                        Response.StatusCode = 401;
                        Response.AddHeader("WWW-Authenticate", "NTLM " + type2message);
                        Email.SendMailToDebugger("Auth", "Type 1 Received, Type 2 Sent");
                        Response.End();
                        }
                    else if (typebyte.ToString("X2") == "03") //type3 message received
                        {
                        var dv = Database.GetDataView("select UPPER('termana'||REPLACE(P.EMAIL,'@termana.com','')||p.init) displayname, 'termana\\'||REPLACE(P.EMAIL,'@termana.com','') username  from tercons.phonebook p where P.COMPANY_ID=40");
                        string username = ""; //magic to get the username from the type3 response
                        Email.SendMailToDebugger("Auth", "Type 3 Received, logging in: " + username);
                        FormsAuthentication.RedirectFromLoginPage(username, false);
                        }
                    else
                        {
                        Email.SendMailToDebugger("Auth", "Unknown Type Received");
                        }
                    }
                else
                    {
                    Email.SendMailToDebugger("Auth", "Unknown Authentication Received: " + Request.Headers["Authorization"]);
                    }
                }
            }
        }

Question

This seems to work fairly well so far. It properly logs in the user if they support IWA. If their browser isn't configured to accept IWA, I want to fall back on Forms Authentication. Unfortunately, what I see happening is if the browser isn't configured to accept IWA, it popups up the ugly NTLM authentication dialog (looks like the Basic Dialog). How do I get that to not appear?

Background

The primary reason I'm doing this is because the same site may be accessed via desktop users (on the domain) or mobile (iPhone/Windows Phone). And iPhone doesn't support saving passwords for the NTLM authentication, which is a hassle for my users.

To Test

If you want to test this code in your own environment, configure a site for forms authentication, make sure Anonymous authentication is checked in IIS, not IWA.

Also

This code is not fully tested/fleshed out. If you're a random person that stumbles on my question, don't assume it's perfectly secure and then go implement it on your site. This code is in the early development stages. That said, if you want to leave a comment saying how to improve it, feel free.

Update

I have updated my code and question to reflect the fact that I managed to get it so that when the user cancels the ugly authentication dialog they're able to log in with forms authentication. But I still want that ugly dialog suppressed.


Solution

  • I suspect the unwanted popup stems from the initial request NOT containing an Authorization header until the 401 is received by the browser. Instead, you need to choose to avoid issuing the 401 if you predict forms authorization is required.

    Consider this approach:

    • Enable Forms authentication as the default mode (not NTLM), and
    • Modify Global.asax to mimic the NTLM authentication if your user agent is not a mobile agent (or whatever combination of IP/user agent restriction you view as constituting NTLM browsers).

    Code in Global.asx would be along these lines.

    Handle Application_AuthenticateRequest explicitly:

    protected void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        try
        {
            if (IsAutomation() && Request.Headers["Authorization"] != null)
            {
                // Your NTML handling code here; below is what I use for Basic auth
                string[] parts = Request.Headers["Authorization"].Split(' ');
                string credentials = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(parts[1]));
                string[] auth = credentials.Split(':');
                if (Membership.ValidateUser(auth[0], auth[1]))
                {
                    Context.User = Membership.GetUser(auth[0]);
                }
                else
                {
                    Response.Clear();
    
                    Response.StatusCode = 401;
                    Response.StatusDescription = "Access Denied";
                    Response.RedirectLocation = null;
                    // Switch to NTLM as you see fit; just my sample code here
                    Response.AddHeader("WWW-Authenticate", "Basic realm={my realm}");
                    Response.ContentType = "text/html";
                    Response.Write(@"
    <html>
    <head>
    <title>401 Access Denied</title>
    </head>
    <body>
    <h1>Access Denied</h1>
    <p>The credentials supplied are invalid.</p>
    </body>
    </html>");
                }
            }
        }
        catch (System.Exception ex)
        {
            throw ex;
        }
    }
    

    Where IsAutomation determines whether you want forms auth or not.

    In my case, IsAutomation looks like this:

    protected bool IsAutomation()
    {
        // In your case, I'd config-drive your desktop user agent strings
        if (!string.IsNullOrEmpty(Properties.Settings.Default.BasicAuthenticationUserAgents))
        {
            string[] agents = Properties.Settings.Default.BasicAuthenticationUserAgents.Split(';');
            foreach (string agent in agents)
                if (Context.Request.Headers["User-Agent"].Contains(agent)) return true;
        }
        return false;
    }
    

    Lastly, you need to trap the 302 redirect and issue a NTLM challenge:

    protected void Application_EndRequest(object sender, EventArgs e)
    {
        if (IsAutomation() && Context.Response.StatusCode == 302)
        {
            Response.Clear();
    
            Response.StatusCode = 401;
            Response.StatusDescription = "Access Denied";
            Response.RedirectLocation = null;
            // Switch to NTLM as you see fit; just my sample code here
            Response.AddHeader("WWW-Authenticate", "Basic realm={your realm}");
            Response.ContentType = "text/html";
            Response.Write(@"
    <html>
    <head>
    <title>401 Authorization Required</title>
    </head>
    <body>
    <h1>Authorization Required</h1>
    <p>This server could not verify that you are authorized to access the document requested.  Either you supplied the wrong credentials (e.g., bad password), or your browser doesn't understand how to supply the credentials required.</p>
    </body>
    </html>");
        }
    }