From 4ca77f10594eee5f455c0850f9597bc23b6c4727 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 24 May 2026 04:25:20 -0700 Subject: [PATCH] Harden msgraph webhook auth requirements (#30169) --- gateway/config.py | 4 ++- gateway/platforms/msgraph_webhook.py | 8 +++++- tests/gateway/test_msgraph_webhook.py | 27 ++++++++++++++++++- .../test_platform_connected_checkers.py | 3 ++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/gateway/config.py b/gateway/config.py index bc077b1994e..0a578f66f34 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -424,7 +424,9 @@ _PLATFORM_CONNECTED_CHECKERS: dict[Platform, Callable[[PlatformConfig], bool]] = Platform.SMS: lambda cfg: bool(os.getenv("TWILIO_ACCOUNT_SID")), Platform.API_SERVER: lambda cfg: True, Platform.WEBHOOK: lambda cfg: True, - Platform.MSGRAPH_WEBHOOK: lambda cfg: True, + Platform.MSGRAPH_WEBHOOK: lambda cfg: bool( + str(cfg.extra.get("client_state") or "").strip() + ), Platform.FEISHU: lambda cfg: bool(cfg.extra.get("app_id")), Platform.WECOM: lambda cfg: bool(cfg.extra.get("bot_id")), Platform.WECOM_CALLBACK: lambda cfg: bool( diff --git a/gateway/platforms/msgraph_webhook.py b/gateway/platforms/msgraph_webhook.py index 46430a25bc7..b7045c801a6 100644 --- a/gateway/platforms/msgraph_webhook.py +++ b/gateway/platforms/msgraph_webhook.py @@ -133,6 +133,12 @@ class MSGraphWebhookAdapter(BasePlatformAdapter): self._notification_scheduler = scheduler async def connect(self) -> bool: + if self._client_state is None: + logger.error( + "[msgraph_webhook] Refusing to start without extra.client_state configured" + ) + return False + app = web.Application() app.router.add_get(self._health_path, self._handle_health) app.router.add_get(self._webhook_path, self._handle_validation) @@ -310,7 +316,7 @@ class MSGraphWebhookAdapter(BasePlatformAdapter): """ expected = self._client_state if expected is None: - return True + return False provided = self._string_or_none(notification.get("clientState")) if provided is None: return False diff --git a/tests/gateway/test_msgraph_webhook.py b/tests/gateway/test_msgraph_webhook.py index d97c98492ae..55eb5f7711f 100644 --- a/tests/gateway/test_msgraph_webhook.py +++ b/tests/gateway/test_msgraph_webhook.py @@ -6,7 +6,7 @@ import json import pytest from gateway.config import GatewayConfig, Platform, PlatformConfig, _apply_env_overrides -from gateway.platforms.msgraph_webhook import MSGraphWebhookAdapter +from gateway.platforms.msgraph_webhook import AIOHTTP_AVAILABLE, MSGraphWebhookAdapter def _make_adapter(**extra_overrides) -> MSGraphWebhookAdapter: @@ -70,6 +70,15 @@ class TestMSGraphWebhookConfig: class TestMSGraphValidationHandshake: + @pytest.mark.anyio + async def test_connect_requires_client_state(self): + if not AIOHTTP_AVAILABLE: + pytest.skip("aiohttp not installed") + adapter = MSGraphWebhookAdapter(PlatformConfig(enabled=True, extra={})) + connected = await adapter.connect() + assert connected is False + assert adapter.is_connected() is False + @pytest.mark.anyio async def test_validation_token_echo_on_get(self): adapter = _make_adapter() @@ -99,6 +108,22 @@ class TestMSGraphValidationHandshake: class TestMSGraphNotifications: + @pytest.mark.anyio + async def test_missing_client_state_is_auth_rejected(self): + adapter = _make_adapter(client_state=None) + payload = { + "value": [ + { + "id": "notif-no-client-state", + "subscriptionId": "sub-1", + "changeType": "updated", + "resource": "communications/onlineMeetings/meeting-1", + } + ] + } + resp = await adapter._handle_notification(_FakeRequest(json_payload=payload)) + assert resp.status == 403 + @pytest.mark.anyio async def test_valid_notification_accepted_and_scheduled(self): adapter = _make_adapter() diff --git a/tests/gateway/test_platform_connected_checkers.py b/tests/gateway/test_platform_connected_checkers.py index 941b8c74506..f7677a3a676 100644 --- a/tests/gateway/test_platform_connected_checkers.py +++ b/tests/gateway/test_platform_connected_checkers.py @@ -79,10 +79,11 @@ def test_checker_returns_true_when_configured(platform, checker, monkeypatch): elif platform in { Platform.API_SERVER, Platform.WEBHOOK, - Platform.MSGRAPH_WEBHOOK, Platform.WHATSAPP, }: mock_config.extra = {} + elif platform == Platform.MSGRAPH_WEBHOOK: + mock_config.extra = {"client_state": "expected-client-state"} elif platform == Platform.FEISHU: mock_config.extra = {"app_id": "app"} elif platform == Platform.WECOM: