Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

mechineKey Element and MVC

0.00/5 (No votes)
2 Apr 2011 1  
I found a secret when publishing your Mvc Web Site to remote server and enable the RoleManager to get the current user roles.

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

  1. I installed the DotNetAge on local server everything works fine.
  2. 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.
  3. 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.

screenshot-iis-machine-key

screenshot-iis-machine-key-gen

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here