Skip to content

Commit fb00974

Browse files
author
Jihaochang
committed
fix(auth): distinguish missing vs invalid token in 401 response
Per RFC 6750 Section 3.1, when no authentication credentials are presented, the server SHOULD NOT include an error code in the WWW-Authenticate header. Instead, it should provide discovery information (resource_metadata and scope). This fixes issue #3769 where both missing-token and invalid-token cases were treated identically with error="invalid_token". Changes: - Add _send_auth_discovery_challenge() for no-credentials case - Check Authorization header presence before sending error - Include scope parameter in discovery challenge per spec - Keep existing error behavior for invalid/expired tokens
1 parent d7845c0 commit fb00974

1 file changed

Lines changed: 51 additions & 2 deletions

File tree

‎src/mcp/server/auth/middleware/bearer_auth.py‎

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,21 @@ def __init__(
115115
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
116116
auth_user = scope.get("user")
117117
if not isinstance(auth_user, AuthenticatedUser):
118-
await self._send_auth_error(
119-
send, status_code=401, error="invalid_token", description="Authentication required"
118+
# Check if there was an Authorization header (indicating invalid token)
119+
# or no header at all (indicating missing credentials)
120+
auth_header = scope.get("headers", [])
121+
has_auth_header = any(
122+
key.lower() == "authorization" for key, _ in auth_header
120123
)
124+
125+
if has_auth_header:
126+
# Invalid/expired token - send error
127+
await self._send_auth_error(
128+
send, status_code=401, error="invalid_token", description="Authentication required"
129+
)
130+
else:
131+
# No credentials provided - send discovery-style challenge per RFC 6750 §3.1
132+
await self._send_auth_discovery_challenge(send)
121133
return
122134

123135
auth_credentials = scope.get("auth")
@@ -132,6 +144,43 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
132144

133145
await self.app(scope, receive, send)
134146

147+
async def _send_auth_discovery_challenge(self, send: Send) -> None:
148+
"""Send a discovery-time 401 challenge without error code (RFC 6750 §3.1).
149+
150+
Per spec: when no authentication information was presented, the server
151+
SHOULD include resource_metadata and scope parameters, but NOT an error code.
152+
"""
153+
www_auth_parts = []
154+
if self.resource_metadata_url:
155+
www_auth_parts.append(f'resource_metadata="{self.resource_metadata_url}"')
156+
if self.required_scopes:
157+
# Join scopes with space per RFC 6750
158+
www_auth_parts.append(f'scope="{" ".join(self.required_scopes)}"')
159+
160+
www_authenticate = f"Bearer {', '.join(www_auth_parts)}" if www_auth_parts else "Bearer"
161+
162+
body = {}
163+
body_bytes = json.dumps(body).encode()
164+
165+
await send(
166+
{
167+
"type": "http.response.start",
168+
"status": 401,
169+
"headers": [
170+
(b"content-type", b"application/json"),
171+
(b"content-length", str(len(body_bytes)).encode()),
172+
(b"www-authenticate", www_authenticate.encode()),
173+
],
174+
}
175+
)
176+
177+
await send(
178+
{
179+
"type": "http.response.body",
180+
"body": body_bytes,
181+
}
182+
)
183+
135184
async def _send_auth_error(self, send: Send, status_code: int, error: str, description: str) -> None:
136185
"""Send an authentication error response with WWW-Authenticate header."""
137186
# Build WWW-Authenticate header value

0 commit comments

Comments
 (0)