Omnissa logo
Engineering
security

Taming the Oversized OAuth2 Token: A First-Hand Tale from a BFF Architecture

SSamar Prakash
July 21st, 2025
Taming the Oversized OAuth2 Token: A First-Hand Tale from a BFF Architecture

Introduction

I vividly recall how a seemingly insignificant detail ended up disrupting our entire authentication flow. As an engineer at Omnissa Intelligence (OI), I was on the front line when our frontend-heavy application (with a Backend-for-Frontend (BFF) API Gateway) started mysteriously failing to authenticate certain users. We’d see login attempts succeed, only for users to be immediately logged out or stuck in reload loops. Diving into logs and browser data, we discovered the culprit: an OAuth2 JWT access token so large that it exceeded browser cookie limits, causing the cookie to be silently dropped. This is the story of how we diagnosed the “giant token” problem, learned from industry insights, and engineered a robust solution using envelope encryption to regain control of our sessions.

The Technical Background: When JWTs Outgrow Cookies

JWT (JSON Web Token) is a popular format for OAuth2 access tokens and ID tokens. It’s essentially a JSON payload (claims about the user) signed (and sometimes encrypted) and encoded. JWTs are self-contained and stateless — meaning the server doesn’t need to store session data for them — which makes them convenient for distributed systems. In our case, after a user logged in via OAuth2, our Authorization Server set an HTTP-only cookie on the browser containing the JWT. This allowed the single-page app (SPA) to automatically include the token on subsequent API calls. The catch? Browsers and infrastructure have limits on cookie size. Most browsers cap each cookie around ~4KB, and some proxies or servers enforce a 4KB header limit for all cookies curity.io. Our token, with all its embedded user info and claims and Scopes, was pushing 4KB+ — beyond the standard limit.

What happens when a cookie is too large? Essentially, the browser will truncate it or refuse to store it. In our case, the oversized token cookie never made it intact to the server, so our gateway couldn’t authenticate the user’s session (no valid token was received). The user would appear logged out immediately or get an endless login redirect cycle. It was a perplexing failure: the session failed on the app because the token didn’t stick. Clearly, JWT tokens should not be so large when stored in cookies — as OAuth2 best practices warn, cookies should carry only small, opaque tokens.

Real-World Encounters with Giant Tokens

It turns out we were not alone in facing this issue. In one case, Argo CD (an open source deployment tool) ran into a 4KB cookie limit when their OIDC provider (Dex) was configured to include all of a user’s GitHub groups in the JWT. The JWT ballooned beyond 4KB and hit the browser’s max cookie size github.com. The result? Exactly the kind of login loop and failures we saw. The Argo team discussed alternatives — from compressing the JWT to switching to a session-ID-in-Redis approach github.com. They even noted (as we did) that storing tokens in localStorage wasn’t a safe option due to XSS risks (a common security warning in OAuth circles).

Our own token’s bloat was caused by similarly large payloads — in our case, an abundance of user roles and permissions and scopes etc encoded in the JWT. Another real-world example: users of OpenSearch Dashboards (Kibana) have reported that if an auth cookie grows beyond ~6000 bytes (for instance, by including 50+ group memberships), it simply breaks the authentication flow forum.opensearch.org. Browsers won’t accept such a cookie, and the application must find another way to handle the session. The take-away from industry experience was clear: if your token is too large for a cookie, you need to redesign how you handle sessions. Many modern architectures recommend using opaque tokens (small random strings) in the browser, and keeping the actual JWT data on the server side. We were essentially about to learn why.

First Attempt: Squeezing the Token with Compression

Our immediate reaction to the problem was pragmatic: if the token is too big, can we shrink it? We implemented JWT compression in cookies. The idea was straightforward — compress the JWT payload (using GZIP) before setting it in the cookie, and decompress it in the API gateway on each request. This indeed reduced the size of our cookie significantly. In cases where the original token was, say, 5.0 KB, compression might bring it down under the 4KB limit. For a moment, it seemed we had sidestepped the browser limit.

However, this “quick fix” came with its own costs and headaches:

In short, while compression technically solved the part of the size issue, it introduced new performance and scalability issues. It felt like a band-aid, not a robust solution. And as our user base grew, even compressed tokens could approach size limits again. We needed a more sustainable strategy — one that aligned with best practices and didn’t fight against the grain of the browser and our framework.

A Better Path Emerges: Going “Opaque” with Server-Side Tokens

Around this time, we revisited OAuth2 fundamentals — the kind you find in resources like “The Nuts and Bolts of OAuth 2.0”. One of the core lessons of OAuth2 (and web security in general) is the trade-off between stateless tokens (JWTs) versus stateful sessions (server-stored tokens). JWTs let you avoid server storage but at the cost of potentially large token sizes and more difficulty in revocation. Server-stored sessions (like the classic “session ID” cookie) keep a small identifier in the client and everything else on the backend, which allows more control at the expense of maintaining state.

Considering our situation, we decided to flip the model: instead of cramming a giant self-contained token into the client, we’d store the token securely on the server and give the client just a small reference to it — effectively an opaque session token. This approach is recommended by many in the OAuth community when dealing with browser limits or security concerns. But we wanted to implement it with a modern, cloud-ready twist: ensuring the token store was distributed and securely encrypted.

Engineering the Solution: Envelope Encryption with Data Keys

Architecture

After brainstorming, our team at OI devised a solution that borrowed from the concept of envelope encryption (often used in cloud security) — but tailored to our needs. We introduced a two-layer token approach using Data Encryption Keys (DEKs):

Illustration — Our original approach (red path) stored a large JWT in the browser’s cookie, which exceeded the ~4KB cookie limit and led to authentication failures. The new approach (green path) issues a small opaque session cookie (containing a DEK) to the browser. The actual JWT is stored encrypted in Redis on the server side. The API Gateway uses the DEK from the cookie to look up and decrypt the JWT for each request. This way, the JWT remains encrypted at rest and never travels in full across the wire or into the browser’s JavaScript context.

Security Wins: No Plaintext Tokens Anywhere

This new design brought immediate relief to our size problem — cookies went from 4KB+ to a 32 bytes — but it also improved our security posture significantly. We achieved several security benefits in one stroke:

Architecture of the final solution — The user authenticates and receives a JWT, but instead of storing it directly, the Auth server generates a random DEK and uses it to encrypt the JWT. The encrypted token is stored in REDIS (distributed cache) with a key that is a hash of the DEK. The browser is given a small, secure cookie containing the raw DEK (session key). On each request, the API Gateway retrieves the encrypted JWT from REDIS via the DEK’s hash and decrypts it on the fly using the DEK from the cookie. This way, the JWT remains encrypted at rest and never travels in full across the wire or into the browser’s JavaScript context.

Scaling Out and Cleaning Up: More Benefits

Beyond solving the immediate token size problem and improving security, our new approach had some happy side-effects on scalability and resource usage:

An Unexpected Win: Solving the JVM Memory Leak

Just as we were beginning to see the benefits of this new token encryption model, another unrelated production issue surfaced — one that appeared to have nothing to do with token size, yet turned out to be directly connected.

Memory Leak

Our authorization service — internally known as the OI Authorization Server — had started experiencing memory pressure. Specifically, we saw an OutOfMemoryError (OOM) in production. This was surprising because the service wasn’t expected to be memory-intensive. After analyzing heap dumps, we discovered that it was holding on to hundreds of thousands of bearer tokens in memory(Expired ones).

Digging deeper, we found the root cause in the default behavior of Spring Security’s InMemoryOAuth2AuthorizationService. This class stores every token generated by the server in a simple Java ConcurrentHashMap, retaining them in plain text, even after expiration. We hadn’t overridden this behavior in our Spring configuration, and over time, especially during a release freeze (when no fresh containers were being cycled), tokens accumulated unchecked.

This was not just a bug — it was a latent architectural vulnerability. Every token generated for the frontend was being stored in memory unnecessarily, causing memory growth that was only mitigated during deployments (when JVMs restarted). Had this gone unnoticed, it could have led to recurring production failures.

Fortunately, because we had already implemented our Redis-backed, envelope-encrypted token storage system to solve the oversized cookie issue, we were able to reuse the exact same mechanism to fix this memory problem. We introduced a new implementation called RedisBackedOAuth2AuthorizationService which:

RedisBackedOAuth2AuthorizationService

With this change, we removed duplicate token storage, improved security posture (no more plaintext tokens in memory), and resolved the OOM condition cleanly. The system now uses Redis as the single source of truth for active tokens, with encrypted data and time-bound retention.

In short, what began as a frontend performance and UX issue ended up surfacing a backend memory leak that we may have otherwise overlooked. Solving the cookie token problem gave us the architecture we needed to resolve this deeper, systemic issue. It was a reminder of how seemingly unrelated problems in distributed systems often have shared roots — and how the right architecture can elegantly solve both.

Reflections and Lessons Learned

In the end, what began as a frantic bug hunt turned into a valuable lesson in architecture and security. We learned that cutting corners with token storage can come back to bite, especially in frontend-heavy architectures. Browsers have their own rules and limits, and we must design within those constraints (or work around them smartly). Our journey led us to essentially modernize the old “server session” concept with a security-focused twist: every session token is encrypted and ephemeral. It’s interesting to note that while JWTs promised to simplify things by being stateless, we ended up re-introducing state — but in a way that scales and remains secure.

For technical leaders and architects, a few takeaways from our experience:

In conclusion, our journey to fix an oversized token bug led us to rebuild our authN/Z architecture into a more robust, scalable system. We transformed a frontend session management issue into an opportunity to bolster security across the board. If you’re grappling with tokens in a frontend-heavy app, consider taking a page from our story: go ahead and tame that giant token — your users, browsers, and servers will thank you for it.