A Look at Cookie Authentication in Katana

Katana provides cookie middleware to serialize user principal into an encrypted cookie and the cookie is used to validate the authenticated user in subsequent request. This post will take a look at Katana's cookie authentication implementation and see the machine key's role in this procedure. The sample used in this post is one ASP.NET MVC5 application created by Visual Studio 2015 Update 3.

After we use web browser to login one form authentication website, we will be able to login automatically next time even reboot the web browser or reboot the machine. Actually, for most commercial web site, even the web server is rebooted, the end user's experience is the same. All of these benefit from cookie and the machine key sit behind (or similar encrypt approach).

Katana has implemented claims-based authentication based on cookie via CookieAuthenticationMiddleware in Microsoft.Owin.Security.Cookies module, it is injected into the Owin pipeline via UseCookieAuthentication. In the same module, CookieAuthenticationHandler class is used to manipulate the cookie:

  • ApplyResponseGrantAsync: write encrypted cookie into http response.
  • AuthenticateCoreAsync: retrieve the cookie from request, decrypt the cookie and generate the claims.

But how the cookie is encrypted? It is controlled by TicketDataFormat which is a field of CookieAuthenticationOptions when configuring the authentication behavior.

 app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
});

Basically, TicketDataFormat has three fields to define how to encrypt, encode and serialize the cookie shown below.

 0:034> !DumpObj /d 0ffd3ad4
Name:        Microsoft.Owin.Security.DataHandler.TicketDataFormat
MethodTable: 07c6886c
EEClass:     0bd9edb0
Size:        20(0x14) bytes
File:        C:\Users\jacky\AppData\Local\Temp\Temporary ASP.NET Files\vs\e0a0c772\9ed39b85\assembly\dl3\98aede97\0052fc3c_4c47d001\Microsoft.Owin.Security.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
07c68ef8  4000008        4 ...Canon, mscorlib]]  0 instance 0ffd36d4 _serializer
07c68bb4  4000009        8 ...on.IDataProtector  0 instance 0ffd3ac4 _protector
07c6ad7c  400000a        c ...oder.ITextEncoder  0 instance 0ffd36ec _encoder
0:034> !DumpObj /d 0ffd3ac4
Name:        Microsoft.Owin.Security.DataProtection.AppBuilderExtensions+CallDataProtectionProvider+CallDataProtection
MethodTable: 07c68c44
EEClass:     0bd9efd8
Size:        16(0x10) bytes
File:        C:\Users\jacky\AppData\Local\Temp\Temporary ASP.NET Files\vs\e0a0c772\9ed39b85\assembly\dl3\98aede97\0052fc3c_4c47d001\Microsoft.Owin.Security.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
07508a64  400000c        4 ...yte[], mscorlib]]  0 instance 0ffd3a84 _protect
07508a64  400000d        8 ...yte[], mscorlib]]  0 instance 0ffd3aa4 _unprotect
0:034> !DumpObj /d 0ffd3a84
Name:        System.Func`2[[System.Byte[], mscorlib],[System.Byte[], mscorlib]]
MethodTable: 07508a64
EEClass:     70f92264
Size:        32(0x20) bytes
File:        C:\windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
7144fb44  40002b5        4        System.Object  0 instance 0ffd3a78 _target
7144fb44  40002b6        8        System.Object  0 instance 00000000 _methodBase
71452bdc  40002b7        c        System.IntPtr  1 instance  c62a938 _methodPtr
71452bdc  40002b8       10        System.IntPtr  1 instance        0 _methodPtrAux
7144fb44  40002c2       14        System.Object  0 instance 00000000 _invocationList
71452bdc  40002c3       18        System.IntPtr  1 instance        0 _invocationCount
0:034> !DumpObj /d 0ffd3a78
Name:        Microsoft.Owin.Host.SystemWeb.DataProtection.MachineKeyDataProtector
MethodTable: 0c500d7c
EEClass:     0c4f501c
Size:        12(0xc) bytes
File:        C:\Users\jacky\AppData\Local\Temp\Temporary ASP.NET Files\vs\e0a0c772\9ed39b85\assembly\dl3\80150423\0025cb3b_4c47d001\Microsoft.Owin.Host.SystemWeb.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
714502f8  4000046        4      System.String[]  0 instance 0ffd3a60 _purposes

In the above, Microsoft.Owin.Host.SystemWeb.DataProtection.MachineKeyDataProtector object is the wrapper class to perform the real encryption operation. In https://katanaproject.codeplex.com/SourceControl/latest\#src/Microsoft.Owin.Host.SystemWeb/DataProtection/MachineKeyDataProtector.cs (shown below), we can clearly see it is using machine key to protect the claim, thus, in order to make web application work in a web farm environment, if the session is not stick to a web server, machine key must be the same between each web server to make sure same cookie can be extracted by different server. Notice that _purposes must be the same to make sure protect and unprotect can work well with each other.

 using System.Text;
using System.Web.Security;

namespace Microsoft.Owin.Host.SystemWeb.DataProtection
{
    internal class MachineKeyDataProtector
    {
        private readonly string[] _purposes;

        public MachineKeyDataProtector(params string[] purposes)
        {
            _purposes = purposes;
        }

        public virtual byte[] Protect(byte[] userData)
        {
            return MachineKey.Protect(userData, _purposes);
        }

        public virtual byte[] Unprotect(byte[] protectedData)
        {
            return MachineKey.Unprotect(protectedData, _purposes);
        }
    }
}

In the following, I have composed a sample code to simulate how claims are extracted from a cookie. I just copy the first two classes from Katana source code repository because they are implemented as internal.

     public class CallDataProtection : IDataProtector
    {
        private readonly Func _protect;
        private readonly Func _unprotect;
        public CallDataProtection(Func protect, Func unprotect)
        {
            this._protect = protect;
            this._unprotect = unprotect;
        }
        public byte[] Protect(byte[] userData)
        {
            return this._protect(userData);
        }
        public byte[] Unprotect(byte[] protectedData)
        {
            return this._unprotect(protectedData);
        }
    }

    public class MachineKeyDataProtector
    {
        private readonly string[] _purposes;
        public MachineKeyDataProtector(params string[] purposes)
        {
            this._purposes = purposes;
        }
        public virtual byte[] Protect(byte[] userData)
        {
            return MachineKey.Protect(userData, this._purposes);
        }
        public virtual byte[] Unprotect(byte[] protectedData)
        {
            return MachineKey.Unprotect(protectedData, this._purposes);
        }
    }

// Call the below code from any controller action in ASP.NET MVC application, AuthenticationTicket will be returned successfully. Notice that I just get the below cookie from my web browser debug, it will be different in your environment.
var keyprotector = new MachineKeyDataProtector(new string[] {
                "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware",
                    "ApplicationCookie",
                    "v1"});
var dataformat = new TicketDataFormat(new CallDataProtection(
                    keyprotector.Protect, keyprotector.Unprotect
                    ));
AuthenticationTicket ticket = dataformat.Unprotect("f8oAAWOXgLSeVNnNLdrjRB8UKXIPxE5zvHToML1QH4e90PJCr9FZkE_oiXvqobrciM1FWw6JTLZw4R0QfgGzoOR3dpD3smN6yEy2Ujs1tTBhAR5w354E-IpTvleumOprvZBvLRluV1pr1EKd2NB9KukO5QG8Gipub4dT_AVVxjYOAauha4R8Zsamm1vK0o7licW6PQoHoLbZ0QVtZpvGOyyvlt39iyYV5u14KI6Aj6881ppFMKaDMhjUM-5zdgZJAd29dFaBjMceTVG1igtGceZ07-ZNFNoQGIdsvF3NcboWEXMrPpPkszb2B4TNFayzZY2a0YBuI2UXWv7a7SkCCUIwx4DEkroNY0T8oTmexSkUkzhyvb1Ad4w0x7WZsL1q0dRphsFSB4JFQNre7v1tj1cvZfWFqDQi7m88Vy4edj7z9b44qc48XADMiZnhyjEWJ4PVvt6hw8N_guhtRAWIw-SfuL_VopWuvYPbnCqexmA");

Cookie is convenient for the authentication process, while in default Katana implementation, it only checks whether the cookie is valid while not verify whether the corresponding user really exists in the user store. So, suppose one end user has valid cookie in multiple devices, then he removes his account from one of the device. Now, his account doesn't exist anymore in the corresponding web site, while he will still be able to login from other devices until the cookie is expired. Though not many web sites provide account remove functionality, it is a good design to valid the user during the login via OnValidateIdentity. The below sample code will reject jacky@contoso.com's login request.

 app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
            OnValidateIdentity = delegate(CookieValidateIdentityContext context)
            {
                return Task.Run(
                    delegate ()
                    {
                        if(context.Identity.Claims.Single(claim => claim.Type == ClaimTypes.Name).Value == "jacky@contoso.com")
                        {
                            context.RejectIdentity();
                        }
                    }
                );
            }
    }
});

In ASP.Net Core, by default, whether it is machine key or DPAPI used to protect the cookie is determined by whether the application is running on ASP.NET or in a different process, you may also have a check on CookieAuthenticationOptions. Notice that ASP.Net MVC and ASP.Net Core applications can share cookie as well, please refer to Sharing cookies between applications for more introduction on it.