Skip to content

Commit 3dbb6b2

Browse files
author
Olivier Gintrand
committed
fix: include resource_metadata in all 401 WWW-Authenticate responses
Move the per-server OAuth enforcement check before the empty-bearer and strict-mode checks in _auth_no_token(), and propagate the enriched WWW-Authenticate header (with RFC 9728 resource_metadata attribute) to all 401 responses. Previously, when a client sent an empty Bearer header to an OAuth-enabled server, the empty-bearer check fired first and returned a generic 'Bearer' WWW-Authenticate without the resource_metadata URL. MCP clients (Open WebUI, VS Code) could not discover the OAuth authorization server to initiate the flow. Now the per-server check always runs first, so if the target server has oauth_enabled=true, the 401 response includes the resource_metadata attribute regardless of which check triggers the rejection. Fixes #3990 Signed-off-by: Olivier Gintrand <olivier.gintrand@forterro.com>
1 parent a2aa82a commit 3dbb6b2

File tree

1 file changed

+11
-11
lines changed

1 file changed

+11
-11
lines changed

mcpgateway/transports/streamablehttp_transport.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3481,15 +3481,11 @@ async def _auth_no_token(self, *, path: str, bearer_header_supplied: bool) -> bo
34813481
Returns:
34823482
True if the request is allowed with public-only access, False if rejected.
34833483
"""
3484-
# If client supplied a Bearer header but with empty credentials, fail closed
3485-
if bearer_header_supplied:
3486-
return await self._send_error(detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"})
3487-
3488-
# Per-server OAuth enforcement MUST run before the global auth check so that
3489-
# oauth_enabled servers always return 401 with resource_metadata URL (RFC 9728).
3490-
# Without this, strict mode (mcp_require_auth=True) returns a generic
3491-
# WWW-Authenticate: Bearer with no resource_metadata, and MCP clients cannot
3492-
# discover the OAuth server to authenticate. (Fixes #3752)
3484+
# Build the WWW-Authenticate header, enriching it with RFC 9728
3485+
# resource_metadata when the target server has OAuth enabled.
3486+
# This must happen before any 401 response so that MCP clients
3487+
# (e.g. Open WebUI, VS Code) can discover the OAuth flow.
3488+
www_auth = "Bearer"
34933489
match = _SERVER_ID_RE.search(path)
34943490
if match:
34953491
per_server_id = match.group("server_id")
@@ -3503,9 +3499,13 @@ async def _auth_no_token(self, *, path: str, bearer_header_supplied: bool) -> bo
35033499
logger.exception("OAuth enforcement check failed for server %s", per_server_id)
35043500
return await self._send_error(detail="Service unavailable — unable to verify server authentication requirements", status_code=503)
35053501

3506-
# Strict mode: require authentication (non-OAuth servers get generic 401)
3502+
# If client supplied a Bearer header but with empty credentials, fail closed
3503+
if bearer_header_supplied:
3504+
return await self._send_error(detail="Invalid authentication credentials", headers={"WWW-Authenticate": www_auth})
3505+
3506+
# Strict mode: require authentication
35073507
if settings.mcp_require_auth:
3508-
return await self._send_error(detail="Authentication required for MCP endpoints", headers={"WWW-Authenticate": "Bearer"})
3508+
return await self._send_error(detail="Authentication required for MCP endpoints", headers={"WWW-Authenticate": www_auth})
35093509

35103510
# Permissive mode: allow unauthenticated access with public-only scope
35113511
# Set context indicating unauthenticated user with public-only access (teams=[])

0 commit comments

Comments
 (0)