Ok I just wasted an hour with a problem related to forms authentication. In my Store I basically lock off my admin directory with authentication requirements which looks something like this:

<authentication mode="Forms">
  <!-- Optional Forms Authentication setup for Admin directory -->
  <forms loginUrl="~/admin/AdminLogin.aspx" timeout="20" path="admin">
    <credentials passwordFormat="Clear">
      <user name="webstoreadmin" password="secret" />
    </credentials>
  </forms>
</authentication>
<identity impersonate="false" />
<!--  AUTHORIZATION 
      This section sets the authorization policies of the application. You can allow or deny access
      to application resources by user or role. Wildcards: "*" mean everyone, "?" means anonymous 
      (unauthenticated) users.
-->
<authorization>
  <!-- Allow all users at the main store level 
       <location> tag is used to restric access to the admin directory
  -->
  <allow users="*" />
</authorization>

This basically does nothing because it essentially allows all users <g>. To lock off the Admin path I then use a <location> tag:

<location path="admin">
   <system.web> 
    <authorization>
       <!-- Deny any unauthenticated users -->
       <!-- allow users="?"/ -->
       <deny users="?" />
    </authorization>
   </system.web>
 </location>

which then denies access to the admin path.

This works fine to pop up the login dialog but unfortunately it never seemed to authenticate - or more accurately it authenticates but never gets off the Login page. I spent a good hour and a half stepping through code and fiddling with the IIS 7 configuration settings. My first thought it was some sort of Authentication Module conflict in IIS 7 which only supports one mode per virtual/application, but that wasn't it.

Finally I stepped into my code to see if the login is at least authenticating:

protected void Login_Authenticate(object sender, AuthenticateEventArgs e)
{
    if (FormsAuthentication.Authenticate(this.Login.UserName, this.Login.Password))
    {
        e.Authenticated = true;
        FormsAuthentication.RedirectFromLoginPage(this.Login.UserName, true);
        Response.End(); // Required or else the page content still renders. LAME!
    }
}

and sure enough the Authenticate() call succeeds and the request actually redirects correctly to the original page.

Finally I dug out Fiddler and started looking at HTTP traces and here's what I got on the Redirect from the Auth call:

HTTP/1.1 302 Found
Cache-Control: private
Content-Length: 144
Content-Type: text/html; charset=utf-8
Location: /wwstore/admin/default.aspx
Server: Microsoft-IIS/7.0
X-AspNet-Version: 2.0.50727
Set-Cookie: .ASPXAUTH=D11F38E3080CB81D313219A8DD972671ED5F363CD81462968E069B0581C0ED0C3011
4744B5E6218C43EE1369FE6E40D56984A6C3A883863C7940D3EF3AA1DAE5F003982644534A1E79EAFA2C95431D
ED04EAC8938E438361C51C4D7F872F1653; expires=Sat, 19-Jan-2008 11:47:17 GMT; path=admin; HttpOnly
X-Powered-By: ASP.NET
Date: Sat, 19 Jan 2008 11:27:17 GMT
Connection: close

<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href="/wwstore/admin/default.aspx">here</a>.</h2>
</body></html>

This basically sets the authentication cookie and redirects back to the original page (admin/default in this case). Looks Ok, right? But can you spot the problem? I didn't, at least not immediately...

When this request hits admin/default.aspx I ended up with this response:

HTTP/1.1 302 Found
Cache-Control: private
Content-Length: 191
Content-Type: text/html; charset=utf-8
Location: /wwstore/admin/AdminLogin.aspx?ReturnUrl=%2fwwstore%2fadmin%2fdefault.aspx
Server: Microsoft-IIS/7.0
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET
Date: Sat, 19 Jan 2008 11:15:09 GMT
Connection: close

<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href="/wwstore/admin/AdminLogin.aspx?ReturnUrl=%2fwwstore%2fadmin%2fdefault.aspx">here</a>.</h2>
</body></html>

which is clearly Forms Authentication picking up the page as unauthenticated. Looking at the page inbound headers though I noticed - no .ASPXAUTH Cookie set. As  a result ASP.NET tries to authenticate again. And so it goes round and round and round...

After a lot of experimenting around I finally figured out my problem remembering some issues I've had in the past with cookies when a path is set on the cookie. Looking at the cookie again I had:

Set-Cookie: .ASPXAUTH=D11F38E3080CB81D313219A8DD972671ED5F363CD81462968E069B0581C0ED0C3011
4744B5E6218C43EE1369FE6E40D56984A6C3A883863C7940D3EF3AA1DAE5F003982644534A1E79EAFA2C95431D
ED04EAC8938E438361C51C4D7F872F1653; expires=Sat, 19-Jan-2008 11:47:17 GMT; path=admin; HttpOnly

Note the path=admin, which as far as the browser is concerned is a bogus path. If anything the path should be the full web path - something like /wwstore/admin - certainly not admin.

The quick way to fix this was to remove the path altogether which creates the cookie on the Web Root:

  <forms loginUrl="~/admin/AdminLogin.aspx" timeout="20">

If a path is really required the thing to use is something like this:

  <forms loginUrl="~/admin/AdminLogin.aspx" timeout="20" path="/wwstore">

and that also works. Unfortunately you can't use ResolveUrl syntax like  ~/wwstore for the path - A fully quailfied literal value is required and it's embedded as is, so it's not really generic. You have to manually give it a valid virtual path or else the browser won't pick up the cookie correctly.

Browsers keep cookies based on the domain plus any part of the path of the current URL. So /wwstore or /wwstore/admin both would work - although /wwstore is probably the right way to do this. But if for whatever reason the virtual gets renamed the cookie would again break.

With Forms Authentication in place though it seems to me it's fairly vital to use a path in the cookie so that multiple applications on the same site don't get mangled. I'm surprised that ASP.NET by default doesn't add a path to the cookie that corresponds to the ApplicationBasePath.

Fun, fun when one stumbles into a self made trap like this and gets to waste an hour or so, ain't it?