Skip to content

[BUG][SCALE]: JWT cookie exceeds browser 4KB limit when user has many team memberships #2757

@crivetimihai

Description

@crivetimihai

Summary

The JWT access token embeds all team memberships (as both namespaces and teams claims) directly into the cookie. When a user belongs to many teams, the resulting Set-Cookie header exceeds the browser's ~4KB cookie limit. The browser silently drops the oversized cookie, causing an authentication failure loop that is indistinguishable from invalid credentials.

Impact

  • Severity: High — users are completely locked out of the admin UI with no actionable error message
  • Affected endpoints: All cookie-based auth (POST /admin/login, SSO callback, password change redirect)
  • API tokens unaffected: Bearer token auth via Authorization header has no size limit (only cookie transport is affected)

Root Cause

create_access_token() in mcpgateway/routers/email_auth.py:126-198 calls user.get_teams() and serializes every team membership into two JWT claims:

  1. namespaces (line 184): ["user:email", "team:slug-1", "team:slug-2", ..., "public"] — string array of all team slugs
  2. teams (line 193, non-admin only): ["id-1", "id-2", ...] — array of all team UUIDs

The same pattern exists in mcpgateway/services/sso_service.py:905-914.

Neither create_access_token() nor set_auth_cookie() (mcpgateway/utils/security_cookies.py) checks the resulting token size.

Reproduction

# User with 347 team memberships → 12.2 KB cookie (3x the 4KB limit)
# User with 50 team memberships → ~3.5 KB cookie (borderline)
# User with 2 team memberships → 0.7 KB cookie (normal)

The 4KB browser cookie limit is per RFC 6265 §6.1 — browsers SHOULD support at least 4096 bytes per cookie. Most browsers enforce exactly this.

Design Flaws Identified

1. namespaces claim is dead weight

The namespaces array is written but never read. The authorization layer (mcpgateway/auth.py) uses normalize_token_teams() which reads the teams claim exclusively. The namespaces claim duplicates team info as slugs, adds per-entry overhead ("team:" prefix), and serves no functional purpose.

2. Unbounded team membership in JWT

All team memberships are serialized regardless of count. The config has max_teams_per_user: 50 (config.py:478) but this limit is not enforced during token creation — only during team join operations (if at all).

3. No token size validation or fallback

set_auth_cookie() blindly sets whatever token it receives. There is no:

  • Pre-flight size check before setting the cookie
  • Warning log when token exceeds safe thresholds
  • Fallback mechanism (e.g., server-side session, token compression, or claim reduction)

4. Silent failure mode

When the browser drops the oversized cookie, the user is redirected back to the login page with ?error=invalid_credentials — identical to a wrong password. There is no way for the user or admin to diagnose the real issue.

5. Admin tokens include all team namespaces unnecessarily

For admin users, the teams claim is intentionally omitted (line 191-193) to enable admin bypass. But the namespaces claim still includes all teams (line 184), bloating the token for no authorization benefit.

Proposed Fix

Short-term (P0 — unblocks affected users)

  1. Remove namespaces claim from create_access_token() and sso_service.py — it is never consumed
  2. Add token size guard in set_auth_cookie(): if token exceeds 3.5KB, log a warning with the user email and team count
  3. Cap teams in JWT: Only include up to N team IDs (e.g., 20) in the teams claim; fetch the full list from DB at request time for users exceeding the cap

Medium-term (proper architecture)

  1. Move team resolution server-side: Instead of embedding teams in the JWT, store a lightweight token (sub + is_admin + jti) in the cookie and resolve team memberships from DB/cache on each request. The auth cache (AUTH_CACHE_TEAMS_ENABLED) already exists for this.
  2. Add cookie size validation: set_auth_cookie() should refuse to set cookies >4KB and return a 500 with a clear error message instead of silently failing.
  3. Consider session-based auth for the admin UI: The admin UI is server-rendered (HTMX) and doesn't need a self-contained JWT. A server-side session ID cookie (~50 bytes) would eliminate the size issue entirely.

Affected Code

File Lines Issue
mcpgateway/routers/email_auth.py 141-193 create_access_token() — unbounded team serialization + dead namespaces claim
mcpgateway/services/sso_service.py 905-914 Same pattern for SSO tokens
mcpgateway/utils/security_cookies.py 20-70 set_auth_cookie() — no size validation
mcpgateway/utils/create_jwt_token.py 156-162 _create_jwt_token() — also adds namespaces
mcpgateway/admin.py 3036, 3048 Callers of create_access_token() for admin UI login
mcpgateway/config.py 478 max_teams_per_user: 50 — not enforced at token creation

Related

  • RFC 6265 §6.1 — cookie size limits
  • mcpgateway/auth.py:148normalize_token_teams() (the only consumer of team data from tokens)
  • mcpgateway/cache/auth_cache.py — existing auth cache that could replace in-token team data
  • SSO service already warns about Entra ID group overage (line 566) but doesn't apply the same concern to its own tokens

Metadata

Metadata

Assignees

Labels

MUSTP1: Non-negotiable, critical requirements without which the product is non-functional or unsafebugSomething isn't workingpythonPython / backend development (FastAPI)securityImproves security

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions