question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Optimize the Authentication Cookie serialization format and add compression

See original GitHub issue

Is 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 through DeflateStream - 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.String by writing a single flag byte, but it doesn’t optimize the other claim-value-types). It also writes out the Issuer and OriginalIssuer for 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 same Issuer and OriginalIssuer values, 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 7BitVlaInteger is written by BinaryWriter.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_token and id_token were both already Base64 values it seemed very silly to me that ASP.NET serializes these Base64 values as strings into AuthenticationProperties.Items and 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 treat access_token and id_token as opaque values, but as my code already passes raw Byte[] into the DeflateStream I 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_token and access_token specifically, but instead tests all strings in AuthenticationProperties.Items to see if they’re Base64 or Base64Url-encoded first before applying the appropriate optimization.

Describe alternatives you’ve considered

  • I didn’t want to use ITicketStore because 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 DeflateStream on 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 TicketSerializer but passing through DeflateStream:
    • Browser cookie size: 3193 bytes (chunking not required).
    • Compressed serialized AuthenticationTicket size: 2280 bytes.
    • Decompressed serialized AuthenticationTicket size: 4948 bytes.
  • Interning Claim strings with DeflateStream:
    • Browser cookie size: 3085 bytes (chunking not required).
    • Compressed serialized AuthenticationTicket size: 2188 bytes.
    • Decompressed serialized AuthenticationTicket size: 3640 bytes.
  • Interning Claim strings with DeflateStream and 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 Claim strings with DeflateStream, preventing double-Base64-encoding of OIDC tokens, eliding common JWT claim names, and omitting id_token:
    • Browser cookie size: 1963 bytes (chunking not required).
    • Compressed serialized AuthenticationTicket size: 1374 bytes.
    • Decompressed serialized AuthenticationTicket size: 1859 bytes.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:15 (10 by maintainers)

github_iconTop GitHub Comments

2reactions
blowdartcommented, Jun 6, 2019

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.

0reactions
daipluspluscommented, Oct 23, 2020

Oh well 😕

Read more comments on GitHub >

github_iconTop Results From Across the Web

Optimize the Authentication Cookie serialization format and ...
Original ASP.NET Core Authentication cookie size using Microsoft.AspNetCore.Authentication.TicketSerializer . Browser cookie size: 7083 bytes ( ...
Read more >
How can I control serialization of ASP.NET Core Cookie ...
I have an ASP.NET Core 2.2 MVC web-application that uses OIDC from a separate website. In Startup.cs , it has: services .AddAuthentication() ....
Read more >
Use cookie authentication without ASP.NET Core Identity
Create an authentication cookie​​ To create a cookie holding user information, construct a ClaimsPrincipal. The user information is serialized ...
Read more >
Memory Optimization for Redis | Redis Documentation Center
MessagePack is an efficient binary serialization format. ... Compressed Field Names are another way to reduce memory used by field names.
Read more >
Distributed Caching — The Only Guide You'll Ever Need
This write-up is an in-depth guide on Distributed Cache. It does cover all the frequently asked questions about it such as What is...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found