Improving ASP.NET Core Security By Putting Your Cookies On A Diet

Improving ASP.NET Core Security By Putting Your Cookies On A Diet

This blog post explores how we can improve the security of your ASP.NET Core authentication security by reducing the size of our cookies.

Problem #1 – Large cookies

When you work with authentication in ASP.NET Core, you typically use the Cookie handler to sign in the user. As a result of the sign-in, it will issue an authentication session cookie and store it in the browser.

However, It is crucial to manage the size of this cookie to prevent it from becoming too large. If you add OpenID Connect support, the cookie can grow even more. The code below shows a typical setup for using OpenID Connect together with the Cookie handler in ASP.NET Core:

				
					builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "cookie";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie", o =>
{
   //...
})
.AddOpenIdConnect("oidc", o =>
{
    o.Authority = "https://myidentityserver.com";
    o.ClientId = "client";

    o.ClientSecret = "mysecret";
    o.ResponseType = "code";

    o.Scope.Clear();
    o.Scope.Add("openid");
    o.Scope.Add("profile");
    o.Scope.Add("email");
    o.Scope.Add("offline_access");

    o.GetClaimsFromUserInfoEndpoint = true;

    o.SaveTokens = true;
    //...
});

				
			

How does the OpenIDConnect and Cookie handler work together?

When the user has authenticated successfully at the authorization server, the user is redirected back to the OpenIdConnect handler. As a result of this redirect, it will issue an AuthenticationTicket [https://github.com/dotnet/aspnetcore/blob/main/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs]. This ticket contains the following information:

It will then ask the Cookie handler to sign in the user represented in this ticket. When it signs in the user, it encrypts this ticket and stores it in the session cookie, as shown in the picture below:

Bild:

When we set the SaveTokens flag to true, we ask the OpenIdConnect handler to include the received tokens (id, access, refresh) inside the AuthenticationProperties object. 

This means that the following is included in the authentication session cookie:

However, we have one problem!

Storing all this data may lead to a significant increase in its size.

We should remember that cookies have a maximum size limit of about 4096 bytes. To learn more about cookie limitations, visit the Browser Cookie Limits page and test it yourself.

When the cookie grows too large, the payload is broken up into multiple cookies, as shown in the image below: 

(Yes, the cookie example above is a bit extreme 😊)

Internally, the ChunkingCookieManager oversees this.

Having too many large cookies will affect the overall web performance because all of these cookies will be included in every request to the server. With HTTP/2 and HTTP/3, large cookies are slightly less of a performance problem due to the various internal performance optimizations that they bring. 

We will look at how we can reduce the size of the cookies later in this blog post.

Problem #2  – Signout

In a typical application, when you sign in the user, the issued cookie contains everything about the user, including the ClaimsPrincipal user object, the authentication properties, and optionally, the tokens. So far, so good!

Head over to my blog post: Exploring what is inside the ASP.NET Core cookies 

 if you want to know more about what is stored inside the authentication cookies.

But what happens when you do a signout?

All that happens is that the cookie handler asks the browser to delete the cookie. That’s all.

You can see for yourself in the source code here. 

The cookie is deleted, but what is the security issue here?

The problem is that the cookie can still be used even after the user has signed out. This means that if you copy the cookie to another browser (or if a hacker gets hold of it), it can still be used to access the associated web application services until the data found inside the cookie expires.

Another complexity is that multiple items have different expiration policies inside the authentication cookie. We have the “user session” and the tokens. These have all different lifetimes and give you access to different things.

So, even though you disable all the tokens or they have expired, your authentication session will still be valid. It can also be possible to still access some resources with just the session alone.

How can we improve this?

The solution

As we have seen above, the authentication ticket is stored inside the cookie by default. 

However, instead of storing the authentication ticket directly in the cookie, we can store the information in a separate cookie session store on the backend.

When implemented, the cookie handler will send the ticket to the store, and in return, it gets a session key back. This key is then stored in the cookie, and this substantially reduces the size of the cookie.

What does the cookie contain?

By disabling the cookie encryption as described in my blog post here, we can see that the content of the cookie is the following:

				
					.....cookie..............Microsoft.AspNetCore.Authentication.Cookies-Session
Id$e51c403c-ab75-4df2-9c6e-167abdb60ac8..................
				
			

(The non-printable characters have been replaced with a dot).

Implementing the session store

To implement the session store, we need to implement the ITicketStore [https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/ITicketStore.cs] interface, which is defined as follows:

				
					/// <summary>
/// This provides an abstract storage mechanic to preserve the identity 
/// information on the server while only sending a simple identifier 
/// key to the client. This is most commonly used to mitigate
/// issues with serializing large identities into cookies.
/// </summary>
public interface ITicketStore
{
    /// <summary>
    /// Store the identity ticket and return the associated key.
    /// </summary>
    Task<string> StoreAsync(AuthenticationTicket ticket);

    /// <summary>
    /// Tells the store that the given identity should be updated.
    /// </summary>
    Task RenewAsync(string key, AuthenticationTicket ticket);

    /// <summary>
    /// Retrieves an identity from the store for the given key.
    /// </summary>
    Task<AuthenticationTicket?> RetrieveAsync(string key);

    /// <summary>
    /// Remove the identity associated with the given key.
    /// </summary>
    Task RemoveAsync(string key);
}

				
			

By implementing this interface, you can then store the data in any store you like, as shown below:

A straightforward in-memory store can be implemented like this:

				
					internal class MySessionStore : ITicketStore
{
    private ConcurrentDictionary<string, AuthenticationTicket> mytickets = new();

    public MySessionStore()
    {
    }

    public Task RemoveAsync(string key)
    {
        Log.Debug("MySessionStore.RemoveAsync Key=" + key);

        if (mytickets.ContainsKey(key))
        {
            mytickets.TryRemove(key, out _);
        }

        return Task.FromResult(0);
    }

    public Task RenewAsync(string key, AuthenticationTicket ticket)
    {
        Log.Debug("MySessionStore.RenewAsync Key=" + key + ", 
        ticket = " + ticket.AuthenticationScheme);

        mytickets[key] = ticket;

        return Task.FromResult(false);
    }

    public Task<AuthenticationTicket> RetrieveAsync(string key)
    {
        Log.Debug("MySessionStore.RetrieveAsync Key=" + key);

        if (mytickets.ContainsKey(key))
        {
            var ticket = mytickets[key];
            return Task.FromResult(ticket);
        }
        else
        {
            return Task.FromResult((AuthenticationTicket)null!);
        }
    }

    public Task<string> StoreAsync(AuthenticationTicket ticket)
    {
        var key = Guid.NewGuid().ToString();
        var result = mytickets.TryAdd(key, ticket);

        if (result)
        {
            string username = ticket?.Principal?.Identity?.Name ?? "Unknown";
            Log.Debug("MySessionStore.StoreAsync ticket=" + username + ", key=" + key);

            return Task.FromResult(key);
        }
        else
        {
            throw new Exception("Failed to add entry to the MySessionStore.");
        }
    }
}

				
			

That’s it!

One implementation using the .NET MemoryCache can be found here. More implementations can be found on Github and on NuGet.

Using the session store

To use it, we need to create an instance and pass it to the SessionStore property, as shown below:
				
					.AddCookie("cookie", o =>
{
    //...
    o.SessionStore = new MySessionStore();
})


				
			

Why using a session store improves security

As discussed, when you sign out, the cookie is removed from the browser by default. However, the cookie is still valid and can be reused until its content expires.

By introducing the session store, the entry stored in it is automatically removed at sign-out. This means that even if the cookie is stolen, it can’t be reused! The cookie handler does this for us automatically.

 

Further possibilities

What other possibilities does the introduction of the session store enable? 

  • We can create a “Where you’re signed in” page, like this page on Google:
  • Metrics and audit logs We could add logging or metrics of all the method calls to the session store to get insights into how the users use our system.
  • Sign out users remotely For example, if an employee leaves your company, you can then delete all the entries in the database for this employee.

Feedback, comments, found any bugs?

Let me know if you have any feedback, anything I missed, or any bugs/typos. You can find my contact details here.

About the author

Hi, I’m Tore! I have been fascinated by computers since I unpacked my first Commodore VIC-20. Today, I enjoy providing freelance development and developer training services, focusing on ASP.NET Core, IdentityServer, OpenID Connect, Architecture, and Web Security. You can connect with me on LinkedIn and Twitter, and you can find out more about me and my services here, as well as my courses for developers, including my course, Introduction to IdentityServer and OpenID-Connect.

Share This Story

About me

My name is Tore Nestenius and I’m a trainer and senior software developer focusing on Architecture, Security and Identity, .NET, C#, Backend, and Cloud, among other things.

Do You Want Tore To Be Your Mentor?

Categories