When you want to blog, here is how to do it. Featuring technical solutions.
Blog HowTo
When you want to blog, here is how to do it. Featuring technical solutions.
Blog HowTo
Does your ASP.NET web site have subdomains and use forms authentication with persistent cookies? Do you host your site or blog on a shared hosting platform? If so, you can 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 you have a blog running blogging software (such as Subtext) that supports multiple blogs. Let's also assume you host at WebHost4Life because that is the assumption most articles on this blog have made so far. (However, this example also applies to all web sites with subdomains and forms authentication with cookies and shared hosting.) Say you have the following subdomains set up:
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.
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). WebHost4Life (and probably other shared hosting providers) sets the cookie domain to be the actual domain specified in the URL. This cookie then belongs to that specific subdomain (the one you are logging in to). There are various security mechanisms that enforce this rule.
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! At least it is wrong for our needs. (However, this behavior is by design. It is not a bug. Any cookie without a subdomain name is available to all subdomains and the primary domain.) 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
ASP.NET retrieves cookies (within 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 -- assuming you do not have control over the cookie domain property, as is currently the case at WebHost4Life.
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:
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.)
Comments
Post new comment