Forms Authentication Cookies and Subdomain Names

Does your ASP.NET web site have subdomains and use forms authentication with persistent cookies? Is your site running on a shared hosting server? If so, you could run into a problem authenticating users because of the way cookie domains work. Here are the details of the problem and steps you can take to solve or prevent the problem.

Security or Login Problems Can Occur

For this example, let's assume your web site is a blog. Say you have a blog running blogging software (such as Subtext) that supports multiple blogs. The problem is not related to any particular blogging software or any other specific software - except ASP.NET. This issue is caused by features of ASP.NET that are working as designed in combination with cookie domain features that seem to be common to shared hosting. This behavior is not caused by a bug in ASP.NET, but it can certainly lead to problems and cause bugs in your application. To illustrate, let's say you have the following subdomains set up:

  • http://example.com (same as http://www.example.com - this is an aggregated blog or a home page)
  • http://racing.example.com (this is your friend's blog about racing)
  • http://csharp.example.com (this is your blog about C# software development)
  • and there could be any number of other subdomains

Assume you have user accounts at the aggregated blog (http://example.com) and the other blogs (e.g., http://csharp.example.com). When you log in to any of the blogs at this domain, ASP.NET writes an authentication cookie containing an authentication ticket for your login.

The Problem

Once you log in to http://example.com, you may find that you cannot log in to any of the subdomains. This happens because the authentication cookies for specific subdomains have a limited cookie scope. However, the authentication cookie for http://example.com is available to the primary domain and all subdomains. In shared hosting, control of the cookie domain property may be outside your control.

Why the Problem Occurs

Let's say you have logged in to http://example.com earlier. Now you are logging in to http://csharp.example.com. When you log in, ASP.NET creates a FormsAuthenticationTicket with your username and some other information (such as IssueDate, Expiration, Version, etc.). This FormsAuthenticationTicket is stored in a cookie (for the type of configuration we are discussing). This cookie belongs to the specific subdomain you are logging in to. (Your shared hosting account may control the cookie domain.) There are various security mechanisms that enforce the availability of cookies to web application code by domain.

In this example, the cookie will be named youruser@csharp.example[1].txt. (The number may not be 1. It is usually 1 or 2.) The cookie will be located at "C:\Documents and Settings\YourUser\Cookies". Your previous cookie (from logging in to the primary domain http://example.com)  will be named similar to youruser@example[1].txt and it will be in the same location.

After you log in, you are transferred to another page (usually some resource that requires you to have logged in). The request for this page causes ASP.NET to execute code for AuthenticateRequest (or OnAuthenticateRequest in your custom code such as an HttpModule, for example). One thing that happens in that code is that the authentication cookie you created when you logged in is retrieved. The code to retrieve a cookie looks similar to this:

        HttpContext.Current.Request.Cookies[cookieName];

where 'cookieName' is defined in web.config like this:

 <authentication mode="Forms">
  <
forms name=".MyCookieName"
 
loginUrl="login.aspx"
 
protection="All"
 
requireSSL="false"
 
slidingExpiration="true"
 
timeout="60" />
</authentication>

ASP.NET (or your custom module) will obtain the cookie value and decrypt the authentication ticket. It will then create an instance of an IPrincipal and assign that IPrincipal to HttpContext.Current.User. The IPrincipal will follow your entire request. That's how it should work anyway.

However, in the situation, the cookie retrieved is the cookie from your previous login to the primary domain. This is the wrong cookie! (However, this behavior is by design. It is not a bug.) This incorrect cookie contains an authentication ticket for the user account at the primary domain. Depending on how your code is written, the IPrincipal user account may not have the correct roles now. It probably doesn't have the correct user name. Obtaining the incorrect authentication ticket can have various serious problems, as you can imagine.

The Solution

I'm assuming you do not have the ability to do anything with the cookie domain property - that will be true on at least some shared hosting platforms. Therefore, this solution does not rely upon altering the cookie domain property. I'm assuming the cookie domain property will be set to the actual domain.

ASP.NET retrieves cookies (for a domain) by name (e.g., ".MyCookieName" as configured above). Configurations like I am describing use the same cookie name for the primary domain and all subdomains. Changing that is one route to a solution.

If the cookie for http://csharp.example.com had a name similar to .MyCookieName.CSharp, then the normal ASP.NET code that retrieves the authentication cookie would work correctly. However, each subdomain needs a uniquely named cookie and there is no way to achieve this using the settings in web.config.

Therefore, you have to implement code to give each authentication cookie a unique name. Here is the approach I took recently. This is from code for the Subtext personal blog publishing platform. I decided to rely on the cookie name configured for forms authentication as the root name and attach various suffixes to this name to distinguish cookies for each subdomain.

The 3 basic steps I took are:

  1. Create the unique cookie name;
  2. Add the cookie to the response with the correct cookie name;
  3. Select the correct cookie by name in AuthenticateRequest.

First, I created a method that would create the suffix and then make a unique cookie name. In the case of Subtext, each subdomain is associated with a specific blog. Therefore, I named the cookies using blog identifiers. In the code below, I get the cookie name configured in web.config, and then I get the blog ID and make a unique cookie name.

private static string GetFullCookieName()
{
   
string cookieName = FormsAuthentication.FormsCookieName;
   
StringBuilder name = new StringBuilder(cookieName);
    name.Append(
".");
   
name.Append(Config.CurrentBlog == null ? "null" : Config.CurrentBlog.Id.ToString());
   
return name.ToString();
}

When setting the authentication ticket, I simply call the method above to get the correct (unique) cookie name. I included the entire method because the code shows something else of interest. As others have discovered, there is no good way to directly obtain the timeout value configured in web.config for cookie timeouts. The way I did it is by having ASP.NET make a temporary authentication cookie for me and then copying the timeout data (and other information too) from that temporary cookie. Once that is done, I simply set the correct cookie name and add the cookie to the response.

NOTE: In ASP.NET 2.0, you may think you can use the strongly typed configuration classes to access values such as the authentication cookie timeout. However, that will not work in partial trust. You are making sure your web app will run in partial trust, right? If you don't want to take my approach for accessing the cookie timeout value, you could duplicate the web.config setting for forms authentication timeout in the appSettings section of your web.config file. Having the same value configured in two places is not ideal, however. That's why I chose the method shown below.

public static void SetAuthenticationTicket(string username, bool persist, params string userData)
{
   
//Getting a cookie this way and using a temp auth ticket
   
//allows us to access the timeout value from web.config in partial trust.
   
HttpCookie authCookie = FormsAuthentication.GetAuthCookie(username, persist);
   
FormsAuthenticationTicket tempTicket = FormsAuthentication.Decrypt(authCookie.Value);
   
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
       
tempTicket.Version, tempTicket.Name, tempTicket.IssueDate,
        tempTicket.Expiration,
//this is how we access the configured timeout value
       
persist, userData, tempTicket.CookiePath);
    authCookie.Value =
FormsAuthentication.Encrypt(authTicket);
    authCookie.Name = GetFullCookieName();
//use our custom cookie name
   
HttpContext.Current.Response.Cookies.Add(authCookie);
}

ASP.NET can exhibit some bizarre behavior when it comes to cookies. Cookies can disappear during debugging and cookies can even be created out of thin air. In fact, the innocent act of getting a cookie from the Response cookie collection can create a cookie in the Request cookie collection! Here is an excellent article by Paul Riley that covers all the details of those gotchas. Now, back to the specific solution.

In your code that handles AuthenticateRequest, you need a way to select the correct authentication cookie by name. I used a method called SelectAuthenticationCookie. The code below shows the call site.

void OnAuthenticateRequest(object sender, EventArgs e)
{
   
HttpCookie authCookie = Subtext.Framework.Security.SelectAuthenticationCookie();
    //the rest of your authetication code would follow this.
}

The method is implemented as shown below. Note that it uses the same GetFullCookieName method created above. In the code below, I use a for-loop, but I assume you may want to use some other type of loop (foreach?). I am trusting the jitter to handle the multiple calls to GetFullCookieName efficiently.

public static HttpCookie SelectAuthenticationCookie()
{
    HttpCookie authCookie = null;
    for (int i = 0; i < HttpContext.Current.Request.Cookies.Count; ++i)
    {
        HttpCookie c = HttpContext.Current.Request.Cookies[i];
        if (c.Name == GetFullCookieName())
        {
            authCookie = c;
           break;
        }
    }
    return authCookie;
}
 

As you can see, the code sets and gets the cookie by using the same GetFullCookieName method. That's all it takes to solve this problem.

If you want to look at a running example of the code, go to the Subtext project and use Subversion to get rev 1762 or later of release 1.9, which is currently located at https://svn.sourceforge.net/svnroot/subtext/branches/Release1.9/. (FYI, I assume this code will change completely in Subtext 2.0 when it is released. It could change before that.)

Some References:

 

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

I am just curious about this statement, I am busy rewriting my site into ASP.net (migrating from PHP), and I am busy splitting certain parts of my site into subdomains, and I will run into this issue soon as well...Isnt this whole problem simply a case of form authentication protection levels inside a web application?Which would mean it would be possible to simply assign the same machinekey to each web application, which would make the authentication cookie available across web applications?