mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 22:15:10 +00:00
Authenticate MCP token requests one way under Basic auth
With client_secret_basic the MCP SDK sets the Basic header and still sends client_id in the body; Linear refuses that as two authentication methods. Token and refresh requests now drop the body client_id when the Basic header is set, as RFC 6749 describes.
This commit is contained in:
1 parent
0d42916d67
commit
33f29cb408
2 files changed
+62
-1
No files matched your search
@@ -4,7 +4,7 @@ import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from fastmcp import Client
|
||||
@@ -771,10 +771,37 @@ class DocsGPTOAuth(OAuthClientProvider):
|
||||
redirect_handler=self.redirect_handler,
|
||||
callback_handler=self.callback_handler,
|
||||
)
|
||||
self.context.prepare_token_auth = self._one_client_authentication(self.context.prepare_token_auth)
|
||||
|
||||
self.auth_url = None
|
||||
self.extracted_state = None
|
||||
|
||||
@staticmethod
|
||||
def _one_client_authentication(prepare: Callable) -> Callable:
|
||||
"""Wrap the SDK's token-request auth so the client authenticates one way.
|
||||
|
||||
With ``client_secret_basic`` the SDK sets the Basic header but leaves
|
||||
``client_id`` in the body, which servers such as Linear reject as a
|
||||
second method; RFC 6749 names the client in the body only when it does
|
||||
not authenticate otherwise.
|
||||
|
||||
Args:
|
||||
prepare: The SDK context's ``prepare_token_auth``.
|
||||
|
||||
Returns:
|
||||
The same function, minus ``client_id`` in the body under Basic auth.
|
||||
"""
|
||||
|
||||
def prepare_token_auth(
|
||||
data: dict[str, str], headers: dict[str, str] | None = None
|
||||
) -> tuple[dict[str, str], dict[str, str]]:
|
||||
data, headers = prepare(data, headers)
|
||||
if headers.get("Authorization", "").startswith("Basic "):
|
||||
data = {k: v for k, v in data.items() if k != "client_id"}
|
||||
return data, headers
|
||||
|
||||
return prepare_token_auth
|
||||
|
||||
def _process_auth_url(self, authorization_url: str) -> tuple[str, str]:
|
||||
"""Process authorization URL to extract state"""
|
||||
try:
|
||||
|
||||
@@ -172,3 +172,37 @@ def test_callback_hands_the_sdk_an_authorization_result():
|
||||
assert isinstance(result, AuthorizationCodeResult)
|
||||
assert (result.code, result.state, result.iss) == ("the-code", "st", "https://mcp.linear.app")
|
||||
redis_client.delete.assert_any_call("mcp_oauth:iss:st")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method, basic, body_client_id",
|
||||
[("client_secret_basic", True, False), ("client_secret_post", False, True), ("none", False, True)],
|
||||
)
|
||||
def test_token_request_uses_one_client_authentication(method, basic, body_client_id):
|
||||
"""Linear registers clients for ``client_secret_basic`` and refuses a token
|
||||
request that also names the client in the body; RFC 6749 wants one method."""
|
||||
from mcp.shared.auth import OAuthClientInformationFull
|
||||
|
||||
from docsgpt.agents.tools.mcp_tool import DocsGPTOAuth
|
||||
|
||||
oauth = DocsGPTOAuth(
|
||||
mcp_url="https://mcp.linear.app/mcp",
|
||||
redis_client=MagicMock(),
|
||||
redirect_uri="https://example.com/callback",
|
||||
user_id="user1",
|
||||
)
|
||||
oauth.context.client_info = OAuthClientInformationFull(
|
||||
client_id="cid",
|
||||
client_secret=None if method == "none" else "secret",
|
||||
token_endpoint_auth_method=method,
|
||||
redirect_uris=["https://example.com/callback"],
|
||||
)
|
||||
|
||||
data, headers = oauth.context.prepare_token_auth(
|
||||
{"grant_type": "authorization_code", "code": "c", "client_id": "cid"},
|
||||
{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
|
||||
assert headers.get("Authorization", "").startswith("Basic ") is basic
|
||||
assert ("client_id" in data) is body_client_id
|
||||
assert data["code"] == "c"
|
||||
Reference in new issue
Block a user