Background
These days, I have been updating www.dotnetage.com to DotNetAge Mvc3 edition. After I uploaded all files and data, I logged in to www.dotnetage.com as usual and FormAuth
cookie was lost in a short time meaning my current login account would auto logout! DotNetAge 2 was updated to Mvc3, so I doubted Razor was the cause of the issue. I Googled it but could not find any answers.
Here's What I Did
- I installed the DotNetAge on local server everything works fine.
- It seems to be a form of authentication fail. I thought the cookie timeout was too short so I changed the forms configuration and try again and again but it stood still. Finally I viewed the cookie detail in my web browser. The form auth cookie had not expired! It was easy to see that forms auth had no problem.
- The ASP.NET will read the cookies when accepting an HTTP request. There are two
HttpModules
responses for authentication: FormsAuthenicationModule
and RoleManagerModule
. I had to read the source code to know about what happened in these modules.
In FormsAuthenicationModule
, there is a private
method named OnEnter()
handling all auth requests:
private void OnEnter(object source, EventArgs eventArgs)
{
this._fOnEnterCalled = true;
HttpApplication application = (HttpApplication) source;
HttpContext context = application.Context;
this.OnAuthenticate(new FormsAuthenticationEventArgs(context));
CookielessHelperClass cookielessHelper = context.CookielessHelper;
if (AuthenticationConfig.AccessingLoginPage(context, FormsAuthentication.LoginUrl))
{
context.SetSkipAuthorizationNoDemand(true, false);
cookielessHelper.RedirectWithDetectionIfRequired(null,
FormsAuthentication.CookieMode);
}
if (!context.SkipAuthorization)
{
context.SetSkipAuthorizationNoDemand
(AssemblyResourceLoader.IsValidWebResourceRequest(
context), false);
}
}
public sealed class HttpRequest
{
public bool IsAuthenticated
{
get
{
return (((this._context.User != null) && (
this._context.User.Identity != null)) &&
this._context.User.Identity.IsAuthenticated);
}
}
}
As we can see from this method, when a user requests an unauthorized URL, it will be redirected to LoginUrl
in the forms configuration element in web.config. DotNetAge uses the Request.IsAuthenticated
property to confirm the authenticated user. So let’s look in the IsAuthenticated
property.
Now we know that the IsAuthenticated
returns from User.Identity
. I think the RoleManagerModule
creates the User.Identity
from role cookie when accepting the HTTP request.
I think when I login, first the role cookie is added to the response but it is lost when I request another page. So I checked my browser and found the cookie name ".MVCROLES" after I logged in and discovered its value is correct. But when I go to another page Request.IsAuthenticated
returns false
and the cookie value is empty, so DotNetAge recognizes the current user is not logged in. But why is the role cookie value empty in the second response?
There is an OnLeave
method in RoleManagerModule
that generates the role cookie and sets the response object:
private void OnLeave(object source, EventArgs eventArgs)
{
HttpApplication application = (HttpApplication) source;
HttpContext context = application.Context;
if (((Roles.Enabled && Roles.CacheRolesInCookie) &&
!context.Response.HeadersWritten) && (((
context.User != null) && (
context.User is RolePrincipal)) && context.User.Identity.IsAuthenticated))
{
if (Roles.CookieRequireSSL && !context.Request.IsSecureConnection)
{
if (context.Request.Cookies[Roles.CookieName] != null)
{
Roles.DeleteCookie();
}
}
else
{
RolePrincipal user = (RolePrincipal) context.User;
if (user.CachedListChanged && context.Request.Browser.Cookies)
{
string str = user.ToEncryptedTicket();
if (string.IsNullOrEmpty(str) || (str.Length > 0x1000))
{
Roles.DeleteCookie();
}
else
{
HttpCookie cookie = new HttpCookie(Roles.CookieName, str);
cookie.HttpOnly = true;
cookie.Path = Roles.CookiePath;
cookie.Domain = Roles.Domain;
if (Roles.CreatePersistentCookie)
{
cookie.Expires = user.ExpireDate;
}
cookie.Secure = Roles.CookieRequireSSL;
context.Response.Cookies.Add(cookie);
}
}
}
}
}
Please note the height row: string str = user.ToEncryptedTicket(); follow ToEncryptedTicket()
method:
[Serializable]
public class RolePrincipal : IPrincipal, ISerializable
{
[SecurityPermission(SecurityAction.Assert,
Flags=SecurityPermissionFlag.SerializationFormatter)]
public string ToEncryptedTicket()
{
if (!Roles.Enabled)
{
return null;
}
if ((this._Identity != null) && !this._Identity.IsAuthenticated)
{
return null;
}
if ((this._Identity == null) && string.IsNullOrEmpty(this._Username))
{
return null;
}
if (this._Roles.Count > Roles.MaxCachedResults)
{
return null;
}
MemoryStream serializationStream = new MemoryStream();
byte[] buf = null;
IIdentity identity = this._Identity;
try
{
this._Identity = null;
new BinaryFormatter().Serialize(serializationStream, this);
buf = serializationStream.ToArray();
}
finally
{
serializationStream.Close();
this._Identity = identity;
}
return CookieProtectionHelper.Encode(Roles.CookieProtectionValue, buf, buf.Length);
}
}
The encrypt ticket value is returned from CookieProtectionHelper.Encode
method:
internal static string Encode(CookieProtection cookieProtection, byte[] buf, int count)
{
if ((cookieProtection == CookieProtection.All) || (
cookieProtection == CookieProtection.Validation))
{
byte[] src = MachineKeySection.HashData(buf, null, 0, count);
if (src == null)
{
return null;
}
if (buf.Length >= (count + src.Length))
{
Buffer.BlockCopy(src, 0, buf, count, src.Length);
}
else
{
byte[] buffer2 = buf;
buf = new byte[count + src.Length];
Buffer.BlockCopy(buffer2, 0, buf, 0, count);
Buffer.BlockCopy(src, 0, buf, count, src.Length);
}
count += src.Length;
}
if ((cookieProtection == CookieProtection.All) || (
cookieProtection == CookieProtection.Encryption))
{
buf = MachineKeySection.EncryptOrDecryptData(true, buf, null, 0, count);
count = buf.Length;
}
if (count < buf.Length)
{
byte[] buffer3 = buf;
buf = new byte[count];
Buffer.BlockCopy(buffer3, 0, buf, 0, count);
}
return HttpServerUtility.UrlTokenEncode(buf);
}
Please note:
buf = MachineKeySection.EncryptOrDecryptData(true, buf, null, 0, count);
OK now I know the Role cookie is encrypted by machine key, so I think the Machine key setting has some problem, so I open the IIS manager and open Machine key settings.
By default, the MachineKey is generated automatically. I checked the “Generate a unique key for each application” option and save.
I restart IIS and login again, and fortunately it seems to be resolved!
Conclusion
I never thought that the Authorization and MachineKey settings were so closely related! There are many default encrypt / decrypt methods in .NET using MachineKeys such as Html.AntiForgeryToken()
and some are unknown, so when we publish the website to a remote server and we have certain features maybe when encrypting/decrypting, the best way is to set the Machine key clearly, then AutoGenerate.
History
- 3rd April, 2011: Initial post