Optimize the Authentication Cookie serialization format and add compression
See original GitHub issueIs your feature request related to a problem? Please describe.
I have a website which uses OIDC authentication with many custom claims (obtained from both the id_token and User Info Endpoint). After the user has signed-in their ASP.NET Authentication cookie was over 7KB in size (split by the chunking cookie manager into 3-4 different cookies).
This was unacceptably large as it hit cookie size limits in some browsers like Safari (and Chrome 74 was giving me warnings too).
The actual information content of the cookie was very low - so I dumped the cookies, decrypted them and looked at how they were being serialized and I noted it could be drastically improved.
Describe the solution you’d like
I implemented my own AuthenticationTicket serializer that dropped the final cookie size from ~7KB to about 2.6KB, all without losing any information.
Here’s what my implementation does:
-
The serialized cookie (as
Byte[]) is passed throughDeflateStream- which is the main space-saver and dropped the size from 7KB to about 3KB. This is done before the cookie is encrypted and Base64 encoded. -
I noticed the serialization of the ticket’s claims was also very inefficient (it does optimize
ClamValueType.Stringby writing a single flag byte, but it doesn’t optimize the other claim-value-types). It also writes out theIssuerandOriginalIssuerfor every claim, which is a long URI in my case - which is wasteful. I fixed this by writing all of the unique strings in all of the claims (except values) to a list of strings (basically interning all of the string values) and referencing them by index using a single byte. This works because in practice there’s never more than 255 unique string values (my OIDC claims had about 15 distinct claim types, 20 claim instances, 4 distinct claim-value-types, and all with the sameIssuerandOriginalIssuervalues, which was 41 string values in total. This shrunk the pre-compression size of the Claims area alone from ~2KB (crazy) to ~300 bytes.-
My binary structure of the Claims section is this (excuse my notation, and the
7BitVlaIntegeris written byBinaryWriter.Write(String))<Byte valueCount> <For value to valueCount> <7BitVlaInteger valueLength> <String value> </For> <Byte claimCount> <For claim to claimCount> <Byte claimTypeIndex> <7BitVlaInteger valueLength> <String claimValue> <Byte claimValueTypeIndex> <Byte claimIssuerIndex> <Byte claimOriginalIssuerIndex> </For>
-
-
Finally, as the
access_tokenandid_tokenwere both already Base64 values it seemed very silly to me that ASP.NET serializes these Base64 values as strings intoAuthenticationProperties.Itemsand then Base64-encodes them again. Remember that Base64 encoding increases the storage size by 33%, so double-Base64 results in a 77% increase in storage requirements. Obviously it’s important to treataccess_tokenandid_tokenas opaque values, but as my code already passes rawByte[]into theDeflateStreamI decided to Base64 decode those values first - this resulted in another 200 byte saving in the final cookie size.- Note that JWTs are not actually Base64-encoded (but are instead dot-separated Base64Url-encoded values) which adds some complexity to handling them. My code doesn’t look for
id_tokenandaccess_tokenspecifically, but instead tests all strings inAuthenticationProperties.Itemsto see if they’re Base64 or Base64Url-encoded first before applying the appropriate optimization.
- Note that JWTs are not actually Base64-encoded (but are instead dot-separated Base64Url-encoded values) which adds some complexity to handling them. My code doesn’t look for
Describe alternatives you’ve considered
-
I didn’t want to use
ITicketStorebecause I don’t have a persistent memory cache available nor did I want to use a database instead. It felt overkill and silly to add all that just to keep the cookie size down when a change of serialization format is all that was necessary. -
I recognize that my trick on preventing the Double-Base64-encoding is probably overkill given the power of the
DeflateStream(as the information-theoretical content is the same), but it did save me 200 bytes which helped get my final cookie size down. -
Performing
DeflateStreamon every request might be computationally expensive - but the optimizations to Claim storage size alone are not particularly expensive. Perhaps each optimization could be a toggle option?
Additional context
My implementation is available at https://github.com/Jehoel/aspnetcore-auth-cookie-optimizations
My implementation does not apply the DeflateStream inside the ChunkingCookieManager because I wanted to have control over the first few bytes of the cookie to set a different constant value so the original TicketSerializer would reject it instead of potentially misinterpreting it. And also because I didn’t want to unintentionally compress other cookies.
I noticed that in ASP.NET Core’s source-code that it needs the cookie format to remain identical to OWIN’s - however my application doesn’t use OWIN (to my knowledge) and doesn’t need that interopability.
I tracked the size of the cookie and its intermediate forms after each optimization step:
- Original ASP.NET Core Authentication cookie size using
Microsoft.AspNetCore.Authentication.TicketSerializer.- Browser cookie size: 7083 bytes (7083 bytes from all chunks combined).
- Serialized AuthenticationTicket size: 4948 bytes.
- Using original
TicketSerializerbut passing throughDeflateStream:- Browser cookie size: 3193 bytes (chunking not required).
- Compressed serialized AuthenticationTicket size: 2280 bytes.
- Decompressed serialized AuthenticationTicket size: 4948 bytes.
- Interning
Claimstrings withDeflateStream:- Browser cookie size: 3085 bytes (chunking not required).
- Compressed serialized AuthenticationTicket size: 2188 bytes.
- Decompressed serialized AuthenticationTicket size: 3640 bytes.
- Interning
Claimstrings withDeflateStreamand preventing double-Base64-encoding of OIDC tokens:- Browser cookie size: 2684 bytes (chunking not required).
- Compressed serialized AuthenticationTicket size: 1883 bytes.
- Decompressed serialized AuthenticationTicket size: 2982 bytes.
Smaller gains were also had by eliding known common JWT Claim strings from serialized interned strings (e.g. name, preferred_username, etc).
I also had code that actually removed id_token while saving all other tokens (like access_token) because my application didn’t need to hold on to id_token. I was annoyed that the OIDC SaveTokens option doesn’t let you choose which tokens you want to save or not. After removing id_token my final cookie size was:
- Interning
Claimstrings withDeflateStream, preventing double-Base64-encoding of OIDC tokens, eliding common JWT claim names, and omittingid_token:- Browser cookie size: 1963 bytes (chunking not required).
- Compressed serialized AuthenticationTicket size: 1374 bytes.
- Decompressed serialized AuthenticationTicket size: 1859 bytes.
Issue Analytics
- State:
- Created 4 years ago
- Comments:15 (10 by maintainers)

Top Related StackOverflow Question
So yea, given that Deflate was the compression used to attack TLS in the CRIME vulnerability, and part of the cookie contents can be under an attackers control I’d be very leery of this the risk versus reward doesn’t seem attractive to me.
Oh well 😕