Click here to Skip to main content
Click here to Skip to main content

mechineKey Element and MVC

, 2 Apr 2011 GPL3
Rate this:
Please Sign up or sign in to vote.
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, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

Share

About the Author

Ray_Liang
Architect DotNetAge
China China
In 1999, I started programming using Delphi, VB, VJ.From 2002 I started with .NET using C#.Since 2005 when i had became an EIP product manager I was focus on EIP and CMS technique. In 2008 i established dotnetage.com and started to shared my ideas and projects online. I believe "No shared no grow"
 
www.dotnetage.com
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 1 PinmemberBill Do14-Nov-12 23:19 
GeneralMy vote of 1 PinmemberSeanlm767-Oct-11 9:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.141015.1 | Last Updated 3 Apr 2011
Article Copyright 2011 by Ray_Liang
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid