mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 22:14:18 +02:00
Bind Discord token verifier to client ID (#3405)
🤖 Generated with GPT-5.2-Codex
This commit is contained in:
parent
b8c37bfa89
commit
216f43d0ba
3 changed files with 60 additions and 2 deletions
|
|
@ -48,6 +48,7 @@ class DiscordTokenVerifier(TokenVerifier):
|
|||
def __init__(
|
||||
self,
|
||||
*,
|
||||
expected_client_id: str,
|
||||
required_scopes: list[str] | None = None,
|
||||
timeout_seconds: int = 10,
|
||||
http_client: httpx.AsyncClient | None = None,
|
||||
|
|
@ -55,6 +56,7 @@ class DiscordTokenVerifier(TokenVerifier):
|
|||
"""Initialize the Discord token verifier.
|
||||
|
||||
Args:
|
||||
expected_client_id: Expected Discord OAuth client ID for audience binding
|
||||
required_scopes: Required OAuth scopes (e.g., ['email'])
|
||||
timeout_seconds: HTTP request timeout
|
||||
http_client: Optional httpx.AsyncClient for connection pooling. When provided,
|
||||
|
|
@ -62,6 +64,7 @@ class DiscordTokenVerifier(TokenVerifier):
|
|||
lifecycle. When None (default), a fresh client is created per call.
|
||||
"""
|
||||
super().__init__(required_scopes=required_scopes)
|
||||
self.expected_client_id = expected_client_id
|
||||
self.timeout_seconds = timeout_seconds
|
||||
self._http_client = http_client
|
||||
|
||||
|
|
@ -121,6 +124,13 @@ class DiscordTokenVerifier(TokenVerifier):
|
|||
user_data = token_info.get("user", {})
|
||||
application = token_info.get("application") or {}
|
||||
client_id = str(application.get("id", "unknown"))
|
||||
if client_id != self.expected_client_id:
|
||||
logger.debug(
|
||||
"Discord token app ID mismatch: expected %s, got %s",
|
||||
self.expected_client_id,
|
||||
client_id,
|
||||
)
|
||||
return None
|
||||
|
||||
# Create AccessToken with Discord user info
|
||||
access_token = AccessToken(
|
||||
|
|
@ -235,6 +245,7 @@ class DiscordProvider(OAuthProxy):
|
|||
|
||||
# Create Discord token verifier
|
||||
token_verifier = DiscordTokenVerifier(
|
||||
expected_client_id=client_id,
|
||||
required_scopes=required_scopes_final,
|
||||
timeout_seconds=timeout_seconds,
|
||||
http_client=http_client,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
"""Tests for Discord OAuth provider."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from key_value.aio.stores.memory import MemoryStore
|
||||
|
||||
from fastmcp.server.auth.providers.discord import DiscordProvider
|
||||
from fastmcp.server.auth.providers.discord import DiscordProvider, DiscordTokenVerifier
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -81,3 +83,45 @@ class TestDiscordProvider:
|
|||
|
||||
# Provider should initialize successfully with these scopes
|
||||
assert provider is not None
|
||||
|
||||
def test_token_verifier_is_bound_to_provider_client_id(
|
||||
self, memory_storage: MemoryStore
|
||||
):
|
||||
"""Test DiscordProvider binds token verifier to the configured client ID."""
|
||||
provider = DiscordProvider(
|
||||
client_id="expected-client-id",
|
||||
client_secret="GOCSPX-test123",
|
||||
base_url="https://myserver.com",
|
||||
jwt_signing_key="test-secret",
|
||||
client_storage=memory_storage,
|
||||
)
|
||||
|
||||
verifier = provider._token_validator
|
||||
assert isinstance(verifier, DiscordTokenVerifier)
|
||||
assert verifier.expected_client_id == "expected-client-id"
|
||||
|
||||
|
||||
class TestDiscordTokenVerifier:
|
||||
"""Test DiscordTokenVerifier behavior."""
|
||||
|
||||
async def test_rejects_token_from_different_discord_application(self):
|
||||
"""Token must be bound to configured Discord client_id."""
|
||||
verifier = DiscordTokenVerifier(expected_client_id="expected-app-id")
|
||||
|
||||
mock_client = AsyncMock()
|
||||
token_info_response = MagicMock()
|
||||
token_info_response.status_code = 200
|
||||
token_info_response.json.return_value = {
|
||||
"application": {"id": "different-app-id"},
|
||||
"user": {"id": "123"},
|
||||
"scopes": ["identify"],
|
||||
}
|
||||
mock_client.get.return_value = token_info_response
|
||||
|
||||
with patch(
|
||||
"fastmcp.server.auth.providers.discord.httpx.AsyncClient"
|
||||
) as mock_client_class:
|
||||
mock_client_class.return_value.__aenter__.return_value = mock_client
|
||||
result = await verifier.verify_token("token")
|
||||
|
||||
assert result is None
|
||||
|
|
|
|||
|
|
@ -245,7 +245,10 @@ class TestDiscordHttpClient:
|
|||
from fastmcp.server.auth.providers.discord import DiscordTokenVerifier
|
||||
|
||||
client = httpx.AsyncClient()
|
||||
verifier = DiscordTokenVerifier(http_client=client)
|
||||
verifier = DiscordTokenVerifier(
|
||||
expected_client_id="test-client-id",
|
||||
http_client=client,
|
||||
)
|
||||
assert verifier._http_client is client
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue