In this blog post, we’ll explore a practical way to enhance the security of your ASP.NET Core applications by reducing the size of authentication cookies. Large cookies can lead to performance issues and security risks, especially when using OpenID Connect or storing sensitive information. By implementing these optimizations, you can keep your application secure, streamlined, and efficient.
Problem #1 – Large cookies in ASP.NET Core
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 a large cookie 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 ASP.NET Core
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.
What happens when you do a signout in ASP.NET Core?
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?
Introducing the SessionStore
As mentioned earlier, the authentication ticket is typically stored inside the cookie by default. However, instead of storing it directly in the cookie, we can move this data to a separate session store on the backend, reducing the cookie size and enhancing security.
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 ASP.NET Core Authentication Cookie contain?
By disabling the cookie encryption as described in my blog post Exploring what is inside the ASP.NET Core cookies, 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 ITicketStore
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:
///
/// 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.
///
public interface ITicketStore
{
///
/// Store the identity ticket and return the associated key.
///
Task StoreAsync(AuthenticationTicket ticket);
///
/// Tells the store that the given identity should be updated.
///
Task RenewAsync(string key, AuthenticationTicket ticket);
///
/// Retrieves an identity from the store for the given key.
///
Task RetrieveAsync(string key);
///
/// Remove the identity associated with the given key.
///
Task RemoveAsync(string key);
}
Implementing this interface allows you to store the data in any backend storage of your choice, as demonstrated below:
Here’s an example of a simple in-memory store implementation in ASP.NET Core:
internal class MySessionStore : ITicketStore
{
private ConcurrentDictionary 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 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 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 of the cookie authentication handler, 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.
Additional Possibilities with a Session Store
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.
Tore’s Newsletter
Be the First to Know! Get notified about my latest blog posts, upcoming presentations, webinars, and more — subscribe today!