From 6288662d1b0e637b2d9054621d700f27baec4bec Mon Sep 17 00:00:00 2001 From: Anneshu Nag Date: Wed, 14 Oct 2026 19:07:08 +0530 Subject: [PATCH] fix(proxy): accept 2048-char OAuth state in MCP gateway DCR flow Gemini on the web sends an 1112-character state which the 1024 cap rejected with a 400. RFC 6749 sets no maximum for state. A 2048-char state still seals into the per-flow cookie inside the ~4KB budget. --- .../mcp_server/gateway_dcr_flow.py | 9 ++-- .../mcp_server/test_gateway_dcr_flow.py | 46 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py b/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py index a895dfed162..07f03381a4c 100644 --- a/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py +++ b/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py @@ -141,10 +141,13 @@ every session-token claim set. Four 256-character ASCII URIs seal to roughly 1.5 the encoded client_id is checked against its own cap before registration succeeds. VS Code registers four callbacks for its web and desktop environments.""" -MAX_STATE_LENGTH: Final = 1024 +MAX_STATE_LENGTH: Final = 2048 """Bound on the client ``state`` sealed into the flow cookie and echoed on the auth-code -redirect. An unbounded ``state`` can push the sealed cookie past the browser's ~4KB cap -(silently dropped, breaking the flow); spec clients send a short opaque value.""" +redirect. RFC 6749 sets no maximum: ``state`` is opaque and must round-trip unchanged, +and deployed clients send values past 1024 characters (Gemini on the web sends 1112). +An unbounded ``state`` can still push the sealed cookie past the browser's ~4KB cap +(silently dropped, breaking the flow); a 2048-character ``state`` alongside typical +client_id and redirect_uri values seals to roughly 3.5KB, inside that budget.""" MIN_CODE_VERIFIER_LENGTH: Final = 43 MAX_CODE_VERIFIER_LENGTH: Final = 128 diff --git a/tests/unit/proxy/_experimental/mcp_server/test_gateway_dcr_flow.py b/tests/unit/proxy/_experimental/mcp_server/test_gateway_dcr_flow.py index c79b2efde5e..d7ef2026da4 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_gateway_dcr_flow.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_gateway_dcr_flow.py @@ -21,6 +21,7 @@ from litellm.proxy._experimental.mcp_server.gateway_dcr_flow import ( GATEWAY_AUTH_CODE_TTL_SECONDS, MANUAL_DELIVERY_AUTH_CODE_TTL_SECONDS, MAX_CLIENT_ID_LENGTH, + MAX_STATE_LENGTH, SUBJECT_TOKEN_TYPES, TOKEN_EXCHANGE_GRANT_TYPE, ConsentTeam, @@ -292,6 +293,49 @@ async def test_authorize_validation_failures_never_redirect_to_client(): assert json.loads(response.body)["error"] == expected_error +@pytest.mark.asyncio +async def test_authorize_accepts_gemini_length_state_and_echoes_it_back(): + """Gemini on the web sends an 1112-character state (issue #46414): authorize must + accept it and the code redirect must echo it back unchanged.""" + client_id = (await _register([REDIRECT_URI]))["client_id"] + long_state: Final = "s" * 1112 + response = aggregate_authorize( + request=_request(query=f"client_id={client_id}"), + client_id=client_id, + redirect_uri=REDIRECT_URI, + state=long_state, + code_challenge=CODE_CHALLENGE, + code_challenge_method="S256", + response_type="code", + session_user_id="u1", + ) + assert response.status_code == 303 + handle, cookies = _flow_cookie_from(response) + assert len(next(iter(cookies.values()))) < 4096 + completed = await complete_connect_flow( + request=_request("/authorize/complete", cookies=cookies, method="POST"), + flow_handle=handle, + session_user_id="u1", + cache=DualCache(), + ) + assert completed.status_code == 303 + returned = parse_qs(urlparse(completed.headers["location"]).query) + assert returned["state"] == [long_state] + + over_cap = aggregate_authorize( + request=_request(query=f"client_id={client_id}"), + client_id=client_id, + redirect_uri=REDIRECT_URI, + state="s" * (MAX_STATE_LENGTH + 1), + code_challenge=CODE_CHALLENGE, + code_challenge_method="S256", + response_type="code", + session_user_id="u1", + ) + assert over_cap.status_code == 400 + assert json.loads(over_cap.body)["error"] == "invalid_request" + + @pytest.mark.asyncio async def test_authorize_without_session_redirects_to_login_with_return_to(): client_id = (await _register([REDIRECT_URI]))["client_id"] @@ -616,7 +660,7 @@ async def test_authorize_rejects_over_long_state(): request=_request(query=f"client_id={client_id}"), client_id=client_id, redirect_uri=REDIRECT_URI, - state="s" * 2000, + state="s" * (MAX_STATE_LENGTH + 1), code_challenge=CODE_CHALLENGE, code_challenge_method="S256", response_type="code",