Skip to content

Commit 6faabbe

Browse files
committed
Apply the request body limit to the OAuth authorization server endpoints
create_auth_routes now wraps its endpoints in RequestBodyLimitMiddleware, so /token, /revoke, /register and POST /authorize answer 413 to bodies over the 4 MiB default before any form or JSON parsing. The limit sits inside the CORS wrapper so browser clients still get CORS headers on the 413; GET and OPTIONS requests pass through untouched.
1 parent f40e845 commit 6faabbe

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

docs/migration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,8 @@ single-exchange requests. Keep the smallest value your application actually need
863863
The SSE transport's message endpoint applies the same limit, configured the same way
864864
(`run(transport="sse", max_request_body_size=...)`, `sse_app(...)`, or
865865
`SseServerTransport(..., max_request_body_size=...)` when you mount the transport yourself), and
866-
answers HTTP 405 to anything other than POST.
866+
answers HTTP 405 to anything other than POST. The OAuth endpoints built by `create_auth_routes`
867+
(`/token`, `/register`, `/revoke`, and POST `/authorize`) are limited to the 4 MiB default.
867868

868869
### Streamable HTTP: lifespan now entered once at manager startup
869870

src/mcp/server/auth/routes.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mcp.server.auth.middleware.client_auth import ClientAuthenticator
1818
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
1919
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
20+
from mcp.server.streamable_http_manager import DEFAULT_MAX_REQUEST_BODY_SIZE, RequestBodyLimitMiddleware
2021
from mcp.shared.auth import JWT_BEARER_GRANT_TYPE, OAuthMetadata, ProtectedResourceMetadata
2122
from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER
2223

@@ -51,12 +52,17 @@ def validate_issuer_url(url: AnyHttpUrl):
5152
ID_JAG_GRANT_PROFILE = "urn:ietf:params:oauth:grant-profile:id-jag"
5253

5354

55+
def _body_limited(handler: Callable[[Request], Response | Awaitable[Response]]) -> ASGIApp:
56+
"""Wrap an endpoint so POST bodies over the default limit are answered with 413 before it runs."""
57+
return RequestBodyLimitMiddleware(request_response(handler), DEFAULT_MAX_REQUEST_BODY_SIZE)
58+
59+
5460
def cors_middleware(
5561
handler: Callable[[Request], Response | Awaitable[Response]],
5662
allow_methods: list[str],
5763
) -> ASGIApp:
5864
cors_app = CORSMiddleware(
59-
app=request_response(handler),
65+
app=_body_limited(handler),
6066
allow_origins="*",
6167
allow_methods=allow_methods,
6268
allow_headers=[MCP_PROTOCOL_VERSION_HEADER],
@@ -102,7 +108,7 @@ def create_auth_routes(
102108
AUTHORIZATION_PATH,
103109
# do not allow CORS for authorization endpoint;
104110
# clients should just redirect to this
105-
endpoint=AuthorizationHandler(provider).handle,
111+
endpoint=_body_limited(AuthorizationHandler(provider).handle),
106112
methods=["GET", "POST"],
107113
),
108114
Route(

tests/server/auth/test_error_handling.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from mcp.server.auth.provider import AuthorizeError, RegistrationError, TokenError
1717
from mcp.server.auth.routes import create_auth_routes
1818
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
19+
from mcp.server.streamable_http_manager import DEFAULT_MAX_REQUEST_BODY_SIZE
1920
from tests.server.mcpserver.auth.test_auth_integration import MockOAuthProvider
2021

2122

@@ -288,3 +289,37 @@ async def test_token_error_handling_refresh_token(
288289
data = refresh_response.json()
289290
assert data["error"] == "invalid_scope"
290291
assert data["error_description"] == "The requested scope is invalid"
292+
293+
294+
_FORM = "application/x-www-form-urlencoded"
295+
296+
297+
@pytest.mark.anyio
298+
@pytest.mark.parametrize(
299+
("path", "content_type"),
300+
[("/token", _FORM), ("/revoke", _FORM), ("/register", "application/json"), ("/authorize", _FORM)],
301+
)
302+
async def test_oversized_request_body_returns_413(client: httpx2.AsyncClient, path: str, content_type: str):
303+
"""Each endpoint that reads a request body rejects one over 4 MiB before parsing it."""
304+
response = await client.post(
305+
path, content=b"x" * (DEFAULT_MAX_REQUEST_BODY_SIZE + 1), headers={"Content-Type": content_type}
306+
)
307+
assert response.status_code == 413
308+
309+
310+
@pytest.mark.anyio
311+
async def test_request_body_within_the_limit_is_still_parsed(client: httpx2.AsyncClient):
312+
"""A small body is passed through to the handler intact: the form is parsed and its fields validated."""
313+
response = await client.post("/token", data={"grant_type": "authorization_code"})
314+
assert response.status_code == 401
315+
assert response.json() == {"error": "invalid_client", "error_description": "Missing client_id"}
316+
317+
318+
@pytest.mark.anyio
319+
async def test_options_preflight_is_not_body_limited(client: httpx2.AsyncClient):
320+
"""CORS preflight requests still get their CORS answer; only POST bodies are limited."""
321+
response = await client.options(
322+
"/token", headers={"Origin": "https://client.example.com", "Access-Control-Request-Method": "POST"}
323+
)
324+
assert response.status_code == 200
325+
assert response.headers["access-control-allow-origin"] == "*"

0 commit comments

Comments
 (0)