Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Forms Authentication and path= in the <forms> Tag


:P
On this page:

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?

Posted in ASP.NET  

The Voices of Reason


 

Richard Deeming
January 21, 2008

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

It's quite easy to write an HttpModule to expand app-relative cookie paths to full virtual paths, and to make sure that the Forms Authentication cookie has the path set to the ApplicationBasePath. You simply need to intercept the PreSendRequestHeaders event and process any cookies in the Response.Cookies collection.

However, the cookie path is case-sensitive, so if you set the path to "/wwstore/admin/" and someone requests "/wwStore/Admin/", the cookie won't be sent.


public sealed class CookiePathModule : IHttpModule
{
private string _applicationPath;

public CookiePathModule()
{
}

public void Dispose()
{
}

public void Init(HttpApplication context)
{
_applicationPath = HttpRuntime.AppDomainAppVirtualPath;
if (null != _applicationPath && 0 == _applicationPath.Length) _applicationPath = null;
context.PreSendRequestHeaders += OnPreSendRequestHeaders;
}

private void OnPreSendRequestHeaders(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
HttpCookieCollection cookies = context.Response.Cookies;
if (null != cookies && 0 != cookies.Count)
{
for (int index = 0; index < cookies.Count; index++)
{
ProcessCookie(cookies[index]);
}
}
}

private void ProcessCookie(HttpCookie cookie)
{
if (null != cookie)
{
string path = cookie.Path;
if (!string.IsNullOrEmpty(path))
{
if (path.StartsWith("~/", StringComparison.Ordinal))
{
cookie.Path = _applicationPath + path.Substring(1);
}
else if ("/" == path && null != _applicationPath)
{
if (FormsAuthentication.FormsCookieName == cookie.Name)
{
cookie.Path = _applicationPath + "/";
}
}
}
}
}
}

Donnie Hale
January 21, 2008

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

Rick,

I'm curious why you're using FormsAuthentication.RedirectFromLoginPage in your authenticate handler. It looks like you're using an asp:Login control and handling the Authenticate event. My app does the same thing, but all it does is set e.Authenticated based on our custom authentication store. If it gets set to true, then the LoggedIn event handler is called and then the Login control itself redirects to the originally requested page. I don't have to explicitly do any redirection.

Am I missing something?

Donnie

Rick Strahl
January 21, 2008

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

@Donnie - Usually I'm not using Login control. I hate the way you end up formatting the Login Control so I usually just create my own form and manually call Authenticate. But in this case the point was to be able to explicitly check whether Authentication worked - if you use the default Login control behavior there's not much you can do to see whether the Auth failed or whether there's something wrong with the messaging as in my case.

@Richard - great idea. Case sensitivity shouldn't be an issue as long as you always handle cookie assignment out of a generic routine <s>. In your case though the cookie would always be formatted a specific way anyway since it goes through the module so that should never be an issue unless someone manually assigns the cookie which IMHO is a bad idea anyway. I tend to do all cookie assignments through a routine that handles this as part of the an application library to ensure the abillity to change names/format etc. FWIW, Richard - I see you posting code occasionally here which is great, but could you use the code formatting please? Makes it much easier to read the code...

Sharas
December 09, 2008

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

Hi,

I was just about to write that path is case sensitive and more wasted hours await you. But I've noticed that Richard is "been there done that" kinda guy:)
Don't understand how you can protect yourself from case sensitivity by dealing with path value in single routine. User can type URL in different casing.

nyhtal
September 09, 2009

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

First off, Rick Strahl you da MAN! I cannot tell you how many times you've saved me. You always have the most helpfull information. BIG LOVE to you my friend.

anyway, thought i'd share this as i was fighting getting multiple applications on the same server from being authenticated by each other! What a headache. But i did manage to figure out an good solution. you can set the machinekey validation in your web.config file to "IsolateApps" so you do not have to worry about pathing or uniquely naming your authentication cookies.

<machineKey validationKey="AutoGenerate,IsolateApps" decryptionKey="AutoGenerate,IsolateApps" validation="SHA1" decryption="Auto" />

monnee
December 02, 2009

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

Man! you have just saved me hours of work. Thank you for your post. It works.

Asif
September 16, 2010

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

Hi,

i am using authorization and authentication in our site for Admin Area and generating FormsAuthenticationTicket when user login in Admin.
we are handling it in Global.asax under Application_AuthenticateRequest
this is working fine but when i publish my website to upload onto the server (Page wise dll)
then Global.asax also covert into dll, and not working on server.
I am posting my code also...Please do some help

Login Page:
protected void btnsubmit_Click(object sender, EventArgs e)
{
if (Page.IsValid)
{
ObjAdminLogin.AdminPassword = txtpassword.Text.Trim();
ObjAdminLogin.AdminUserName = txtusername.Text.Trim();
DataTable dt = ObjAdminLogin.FetchData().Tables[0];
if (dt.Rows.Count != 0)
{
DataRow row = dt.Rows[0];
Session["UserName"] = txtusername.Text.Trim();
Session["AdminID"] = ObjAdminLogin.AdminID;
Session["DeptID"] = row["DeptID"].ToString();

if (row["DeptID"].ToString() == "2")
{
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket
(1, txtusername.Text.ToString().Trim(), DateTime.Now,
DateTime.MaxValue, false, "Admin");
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
Response.Cookies.Add(authCookie);

}
else if (row["DeptID"].ToString() == "1")
{
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket
(1, txtusername.Text.ToString().Trim(), DateTime.Now,
DateTime.MaxValue, false, "Support");
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
Response.Cookies.Add(authCookie);
}
else if (row["DeptID"].ToString() == "3")
{
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket
(1, txtusername.Text.ToString().Trim(), DateTime.Now,
DateTime.MaxValue, false, "Marketing");
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
Response.Cookies.Add(authCookie);
}
else if (row["DeptID"].ToString() == "4")
{
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket
(1, txtusername.Text.ToString().Trim(), DateTime.Now,
DateTime.MaxValue, false, "Merchandise");
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
Response.Cookies.Add(authCookie);
}
else if (row["DeptID"].ToString() == "5")
{
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket
(1, txtusername.Text.ToString().Trim(), DateTime.Now,
DateTime.MaxValue, false, "Logistics");
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
Response.Cookies.Add(authCookie);
}

if (Request.QueryString["ReturnUrl"] == null)
{
Response.Redirect("~/Welcome.aspx");
}
else
{
Response.Redirect(Request.QueryString["ReturnUrl"]);
}
}
else
{
lblmsg.Text = ObjAdminLogin.Message;
lblmsg.Visible = true;
}
}
}
web.config:
<authentication mode="Forms">
<forms loginUrl="~/Welcome.aspx" name=".ASPNETAUTH" slidingExpiration="true" timeout="90" protection="All" path="~/"></forms>
</authentication>
<location path="create-sale">
<system.web>
<authorization>
<allow roles="Admin,Merchandise"/>
<deny roles="Support,Marketing,Logistics"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
<location path="user-management">
<system.web>
<authorization>
<allow roles="Marketing"/>
<deny roles="Support,Logistics,Merchandise"/>
<allow roles="Admin"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
Global.aspx:
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
HttpCookie authcookie = Request.Cookies[FormsAuthentication.FormsCookieName];
FormsAuthenticationTicket authticket = null;
authticket = FormsAuthentication.Decrypt(authcookie.Value);
string[] roles;
ArrayList arrroles = new ArrayList();
foreach (string userdata in authticket.UserData.Split(';'))
{
arrroles.Add(userdata);
}
roles = (string[])arrroles.ToArray(typeof(string));
FormsIdentity id = new FormsIdentity(authticket);
System.Security.Principal.GenericPrincipal principal = new System.Security.Principal.GenericPrincipal(id, roles);
Context.User = principal;
}
}

all the dll are in BIN Folder with Global.asax

Regards
Asif

ravi
October 21, 2010

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

I have a problem. it seems my auth cookie and session cookies are getting saved to a machine level folder. What is happening is after I login in using an instance of web browser and then open another instance of web browser and navigate to the same url the I get logged in automatically. What could be wrong?

Any help?

Regards

ravi
October 25, 2010

# re: Forms Authentication and path= in the &lt;forms&gt; Tag

I found out that the above behavior is by web design. So, if someone is looking for a solution for the same above problem, please do not. This is how the websites are designed one session per machine.
we solved the above problem by adding a hidden filed to all webpages and initializing it with a random unique no. we check for this filed in all requests if that is available and the value in it is the one we have in our session object, then the request is coming from the same browser else not.

Regards,

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024