From a215fa7c07dae0ed4a8acadd92efc8b332b32d55 Mon Sep 17 00:00:00 2001 From: Jihaochang Date: Wed, 28 Oct 2026 16:13:51 +0800 Subject: [PATCH] fix(auth): send auth challenge without error when no token is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per RFC 6750 §3.1, when a request lacks any authentication, the resource server SHOULD NOT include an error code. The current code returns error="invalid_token" for both missing and invalid tokens, which prevents clients from distinguishing between the two cases. This change: - Detects whether an Authorization header was present - For missing credentials: returns challenge with resource_metadata and scope (no error code) - For invalid credentials: keeps the existing invalid_token response Fixes #3769 --- src/mcp/server/auth/middleware/bearer_auth.py | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/mcp/server/auth/middleware/bearer_auth.py b/src/mcp/server/auth/middleware/bearer_auth.py index 1a070cd29d..6b65ed0f94 100644 --- a/src/mcp/server/auth/middleware/bearer_auth.py +++ b/src/mcp/server/auth/middleware/bearer_auth.py @@ -115,9 +115,20 @@ def __init__( async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: auth_user = scope.get("user") if not isinstance(auth_user, AuthenticatedUser): - await self._send_auth_error( - send, status_code=401, error="invalid_token", description="Authentication required" - ) + # Check if the request had an Authorization header at all + headers = scope.get("headers", []) or [] + had_auth = any(h == b"authorization" for h, _ in headers) + + if had_auth: + # Invalid token - send error="invalid_token" + await self._send_auth_error( + send, status_code=401, error="invalid_token", description="Authentication required" + ) + else: + # No auth header - send challenge without error code per RFC 6750 + await self._send_auth_challenge( + send, status_code=401 + ) return auth_credentials = scope.get("auth") @@ -163,3 +174,38 @@ async def _send_auth_error(self, send: Send, status_code: int, error: str, descr "body": body_bytes, } ) + + async def _send_auth_challenge(self, send: Send, status_code: int) -> None: + """Send an authentication challenge without error code (per RFC 6750 §3.1). + + When no credentials are presented, the server SHOULD NOT include an error + code. Instead, it should return the resource metadata and required scopes + so the client can perform its own scope selection. + """ + www_auth_parts = [] + if self.resource_metadata_url: + www_auth_parts.append(f'resource_metadata="{self.resource_metadata_url}"') + if self.required_scopes: + www_auth_parts.append(f'scope="{" ".join(self.required_scopes)}"') + + www_authenticate = f"Bearer {', '.join(www_auth_parts)}" if www_auth_parts else "Bearer" + + body = b"" + await send( + { + "type": "http.response.start", + "status": status_code, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", b"0"), + (b"www-authenticate", www_authenticate.encode()), + ], + } + ) + + await send( + { + "type": "http.response.body", + "body": body, + } + )