RSS

Monthly Archives: August 2012

IIS “Mixed Authentication”: Securing a web site with Basic Authentication and Forms Authentication

I have come across a situation where I needed to secure a specific web service with Basic Authentication, in a web site that is secured using Forms Authentication. Unfortunately in IIS this is not as trivial as I hoped it would be. This isn’t another case of having a website for Intranet and Internet users, thus attempting to configure your website to both Forms authentication and Windows authentication and Single Sign-On (SSO) which you can read about here or here. In this case, the website is secured with Forms Authentication but exposes a web service for mobile devices to consume, so no Intranet and SSO are relevant. Authentication is to be performed in this case using Basic Authentication, which means that the client (mobile) has to send over an Authorization header with a Base64 encoding of the username and password (note that the username and password are not secured in this scenario as they are not encrypted, but you can use SSL).

I really banged my head over this one. I tried quite a few options to get this to work using standard IIS configuration. I listed the most noticeable below:

Create a virtual folder and change the authorization rules for that folder
This was truly my preferred solution. If you could have taken a specific file or folder and provide it with different authentication settings, the solution could have been simple and straight forward. Unfortunately IIS shows the following warnings when you try that:

What you see here is a MixedAuth web application that is setup for Forms Authentication, and a ‘Sec’ folder which should have been secured with Basic Authentication. As you can see, when setting Basic Authentication to Enabled and clicking on the Forms Authentication in order to disable it (after successfully disabling anonymous authentication, IIS displays two reservations:

  1. The conflicting authentication modes “cannot be used simultaneously”.
  2. Forms Authentication is readonly and cannot be disabled.

You’ll get the same errors if you try securing the other way around: The web app secured with Basic Authentication and a virtual folder secured with Forms Authentication.

Create a secured web application and change the authorization rules for that folder
This looks like the next best thing. I thought that just like you can create a web application under “Default Web Site”, and change it’s authorization settings to whatever you require, why not make a “sub” web application instead of a virtual folder, and change it’s settings completely. Here goes:

Voila! I thought I had it. So easy! so trivial! so wrong…

Yes, this mode will provide you with a “mixed authentication mode”, but…

  1. These are entirely two different applications. Even if you choose the same application pool for both, they have different App Domains, different Sessions etc.
  2. Even if you decide that it is OK that these are entirely different applications, they do not share the same Bin or App_Code folders. So, if you rely on those folders in your code, you’ll have to duplicate them (and no, creating a Bin virtual folder under “Sec” and pointing it towards the physical path of the Bin under “MixedAuth” will not work).

In other words, creating a “sub web application” is no different than creating any other application and therefore is unlikely to answer your needs.

Enter MADAM
I decided to google some more for possible solutions and found an interesting article hosted in MSDN which was written in 2006 (ASP.NET 1 to 2). The proposed solution is an assembly called MADAM (“Mixed Authentication Disposition ASP.NET Module”), which basically provides a simple solution:

MADAM’s objective is to allow a page developer to indicate that, under certain conditions, Forms authentication should be muted in favor of the standard HTTP authentication protocol.

In other words, using a Module, certain configuration will determine whether a page will be processed using Forms Authentication or Basic Authentication (or another). Here’s another quote that can help understanding this:

Much like how FormsAuthenticationModule replaces the 401 status returned by the authorization module with a 302 status code in order to redirect the user to the login page, MADAM’s HTTP module reverses the effect by switching the 302 back to a 401.

Custom solution
The MADAM article is quite thorough, so if you would like to skip it and to jump right in, I suggest that you download the code sample at the beginning of the article. It contains not only the source code for MADAM but a web.config which you can copy-paste from the desired configuration. In my particular case I needed a different authentication module than the ones supported by MADAM, and therefore I thought that perhaps I should implement a more custom and straightforward solution that serves my needs.

To my understanding, the only way I could combine “Mixed Authentications” was to have my website set to Anonymous Authentication in IIS, and use Global.asax to restrict access to certain pages by returning a 401 (Classic App Pool). For those particular pages, if the user is not already logged in, I would check if there’s an Authorization request header and perform authentication as required. It is important to note, that how you perform the authentication on the server side is completely up to you and will not be performed by IIS in such a case. Therefore I have included within the following code several authentication methods samples that you need to choose from (or add your own). This code is embedded within Global.asax and is compatible with a Classic Application Pool. For Integrated Application Pool you may choose an alternative and use HttpModule to accomplish similar behavior.

Note: This following code supports and follows Basic Authentication “protocol”. Just as a reminder, this means that the client sends an Authorization request header with the word “Basic” and the credentials (“username:password”) encoded as a base64 string (e.g. “Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==”). If you require something different, you will have to adjust the code accordingly (explanation is below the code).

<%@ Application Language="C#" %>
<%@ Import Namespace="System.DirectoryServices.AccountManagement" %>

    void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        if (User == null || !User.Identity.IsAuthenticated)
        {
            string page = Request.Url.Segments.Last();
            if ("Secured.aspx".Equals(page, StringComparison.InvariantCultureIgnoreCase) || Request.Url.Segments.Any(s=>s.Equals("WSSecured.asmx/", StringComparison.InvariantCultureIgnoreCase)))
            {
                bool authorized = false;
                string authorization = Request.Headers["Authorization"];
                if (!string.IsNullOrWhiteSpace(authorization))
                {
                    string[] parts = authorization.Split(' ');
                    if (parts[0] == "Basic")//basic authentication
                    {
                        authorization = UTF8Encoding.UTF8.GetString(Convert.FromBase64String(parts[1]));
                        parts = authorization.Split(':');
                        string username = parts[0];
                        string password = parts[1];

                        // TODO: perform authentication
                        //authorized = FormsAuthentication.Authenticate(username, password);
                        //authorized = Membership.ValidateUser(username, password);
                        using (PrincipalContext context = new PrincipalContext(ContextType.Machine))
                        {
                            authorized = context.ValidateCredentials(username, password);
                        }

                        if (authorized)
                        {
                            HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity(username), null);
                            FormsAuthentication.SetAuthCookie(HttpContext.Current.User.Identity.Name, false);
                        }
                    }
                }

                if (!authorized)
                {
                    HttpContext.Current.Items["code"] = 1;
                    Response.End();
                }
            }
        }
    }

    void Application_EndRequest(object sender, EventArgs e)
    {
        if (HttpContext.Current.Items["code"] != null)
        {
            Response.Clear();
            Response.AddHeader("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", Request.Url.Host));
            Response.SuppressContent = true;
            Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
            Response.End();
        }
    }

Some explanation for the code above:

  • Line 6: attempt authentication only if the user is not authenticated.
  • Lines 8-9: Secured.aspx and WSSecured.asmx are the secured areas. All other pages should be processed as usual using Forms Authentication. Naturally, you need to replace this with something adequate to your needs and less ugly.
  • Lines 12-13 checks whether the Authorization header exists.
  • Lines 16-21 retrieve the username and password from the Authorization header.
  • Line 24 demonstrates how to accomplish authentication using Forms Authentication.
  • Line 25 demonstrates how to use Membership to perform authentication (this could be built-in ASP.NET membership provides or a custom membership.)
  • Lines 26-29 (and line 2) are required if you would like to perform Authentication using Windows Accounts (currently it is set to local server accounts, but you can change the ContextType to domain).
  • Lines 31-35 are processed if authentication was successful. In this case, I used the idea from MADAM to set the User to a Generic Principal, but you may choose to replace this with a Windows Principal if you use actual Windows Accounts. I also set the FormsAuthentication cookie in order to prevent unnecessary authentications for those clients that support cookies.
  • Lines 39-43: If not authenticated, end the response and indicate that a 401 is to be returned. Setting the 401 in this location will not work with Forms Authentication, because Forms Authentication would change it to 302 (redirect to the login page). So we change the return code to 401 in the actual End Request event.
  • Lines 48-58 change the return code to 401 and return a WWW-Authenticate response header, which tells the client which authentication methods are supported by the server. In this case we tell the client that Basic Authentication is supported. For a browser, this will cause a credentials dialog to popup, and the browser will encode typed-in credentials to a Base64 string and send it over in the Authorization request header.

As you can see below, at first, the browser receives a 401 and a WWW-Authenticate header. The browser pops-up the credentials dialog as expected:

And when we type-in the credentials and hit OK, authentication is processed as expected and we receive the following:

As you can see, the browser encoded the credentials to a Base64 string and sent it with “Basic” in the Authorization header, thus indicating that the client wishes to perform authentication using Basic Authentication.

Here’s another example for a client, this time not a browser but a simple console application:

using System;
using System.Net;
using System.Text;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            using (WebClient client = new WebClient())
            {
                string value = Convert.ToBase64String(UTF8Encoding.UTF8.GetBytes("Aladdin:open sesame"));
                client.Headers.Add("Authorization", "Basic " + value);
                string url = "http://localhost/MixedAuth/WSSecured.asmx/HelloWorld";
                var data = client.DownloadString(url);
            }
        }
    }
}

The C# console application above is attempting to consume the secured Web Service. If not for the request header in line 14, we would have gotten a 401.

Summary

I would have preferred it if IIS would have supported different authentication settings in the same Web Application, without requiring custom code or configuring a different application for secure content. From a brief examination IIS8 is no different so this workaround will probably be relevant there too. If you have any better idea or an alternate solution, please comment.

 
3 Comments

Posted by on 24/08/2012 in Software Development

 

Tags: ,