From 0b3b8930f21b3d328ac0f263ced6c54deab744f7 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sat, 4 Apr 2026 23:17:43 -0700 Subject: [PATCH 1/4] fix: reject incoming requests when no credentials are configured If a server is set up without any credentials, it currently will not be able to send messages, but it will accept all incoming requests. This scenario is rare, but possible, and in this PR we reject any incoming requests if credentials are not configured (and if skip_auth isn't explicitly set to True). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../microsoft_teams/apps/http/http_server.py | 48 +++++++++------ packages/apps/tests/test_http_server.py | 60 ++++++++++++++++++- 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/packages/apps/src/microsoft_teams/apps/http/http_server.py b/packages/apps/src/microsoft_teams/apps/http/http_server.py index 8aab280b..48b071c9 100644 --- a/packages/apps/src/microsoft_teams/apps/http/http_server.py +++ b/packages/apps/src/microsoft_teams/apps/http/http_server.py @@ -77,6 +77,12 @@ def initialize( if app_id and not skip_auth: self._token_validator = TokenValidator.for_service(app_id) logger.debug("JWT validation enabled for %s", self._messaging_endpoint) + elif not skip_auth: + logger.warning( + "No credentials configured and skipAuth is not enabled. " + "All incoming requests will be rejected. Configure client authentication " + "to securely receive messages, or set skip_auth=True for local development." + ) self._adapter.register_route("POST", self._messaging_endpoint, self.handle_request) self._initialized = True @@ -90,24 +96,10 @@ async def handle_request(self, request: HttpRequest) -> HttpResponse: # Validate JWT token authorization = headers.get("authorization") or headers.get("Authorization") or "" - if self._token_validator and not self._skip_auth: - if not authorization.startswith("Bearer "): - return HttpResponse(status=401, body={"error": "Unauthorized"}) - - raw_token = authorization.removeprefix("Bearer ") - service_url = cast(Optional[str], body.get("serviceUrl")) - - try: - await self._token_validator.validate_token(raw_token, service_url) - except Exception as e: - logger.warning(f"JWT token validation failed: {e}") - return HttpResponse(status=401, body={"error": "Unauthorized"}) - - token: TokenProtocol = cast(TokenProtocol, JsonWebToken(value=raw_token)) - else: - # No auth — use a default token + if self._skip_auth: + # Auth explicitly skipped — use a default token service_url = cast(Optional[str], body.get("serviceUrl")) - token = cast( + token: TokenProtocol = cast( TokenProtocol, SimpleNamespace( app_id="", @@ -119,6 +111,28 @@ async def handle_request(self, request: HttpRequest) -> HttpResponse: is_expired=lambda: False, ), ) + elif not self._token_validator: + # No credentials configured — reject the request + logger.error( + "No credentials configured. Configure client authentication " + "to securely receive messages, or set skip_auth=True to allow " + "unauthenticated requests." + ) + return HttpResponse(status=401, body={"error": "Authentication not configured"}) + else: + if not authorization.startswith("Bearer "): + return HttpResponse(status=401, body={"error": "Unauthorized"}) + + raw_token = authorization.removeprefix("Bearer ") + service_url = cast(Optional[str], body.get("serviceUrl")) + + try: + await self._token_validator.validate_token(raw_token, service_url) + except Exception as e: + logger.warning(f"JWT token validation failed: {e}") + return HttpResponse(status=401, body={"error": "Unauthorized"}) + + token = cast(TokenProtocol, JsonWebToken(value=raw_token)) core_activity = CoreActivity.model_validate(body) activity_type = core_activity.type or "unknown" diff --git a/packages/apps/tests/test_http_server.py b/packages/apps/tests/test_http_server.py index bba4bcd8..651f0142 100644 --- a/packages/apps/tests/test_http_server.py +++ b/packages/apps/tests/test_http_server.py @@ -131,6 +131,64 @@ async def test_handle_activity_no_handler(self, server): assert result["status"] == 500 +class TestHttpServerNoCredentials: + """Test cases for HttpServer when no credentials are configured and skipAuth is not set.""" + + @pytest.fixture + def mock_adapter(self): + adapter = MagicMock() + adapter.register_route = MagicMock() + adapter.start = AsyncMock() + adapter.stop = AsyncMock() + return adapter + + @pytest.fixture + def server(self, mock_adapter): + server = HttpServer(mock_adapter) + server.initialize(credentials=None, skip_auth=False) + return server + + @pytest.mark.asyncio + async def test_rejects_request_when_no_credentials(self, server): + """Test that requests are rejected with 401 when no credentials are configured.""" + server.on_request = AsyncMock(return_value=InvokeResponse(status=200)) + + request = HttpRequest( + body={ + "type": "message", + "id": "test-123", + "serviceUrl": "https://attacker.com", + }, + headers={}, + ) + + result = await server.handle_request(request) + + assert result["status"] == 401 + assert result["body"] == {"error": "Authentication not configured"} + server.on_request.assert_not_called() + + @pytest.mark.asyncio + async def test_rejects_request_with_auth_header_when_no_credentials(self, server): + """Test that even requests with auth headers are rejected when no credentials are configured.""" + server.on_request = AsyncMock(return_value=InvokeResponse(status=200)) + + request = HttpRequest( + body={ + "type": "message", + "id": "test-123", + "serviceUrl": "https://example.com", + }, + headers={"authorization": "Bearer some-token"}, + ) + + result = await server.handle_request(request) + + assert result["status"] == 401 + assert result["body"] == {"error": "Authentication not configured"} + server.on_request.assert_not_called() + + class TestFastAPIAdapter: """Test cases for FastAPIAdapter.""" @@ -145,7 +203,7 @@ def test_register_route(self): """Test route registration on FastAPI app.""" adapter = FastAPIAdapter() - async def handler(req: HttpRequest) -> HttpResponse: + async def handler(request: HttpRequest) -> HttpResponse: return HttpResponse(status=200, body=None) adapter.register_route("POST", "/test", handler) From 0c08324a17ec366c46c1bac8253169f933c42439 Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:45:00 -0700 Subject: [PATCH 2/4] Update http_server.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/apps/src/microsoft_teams/apps/http/http_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/src/microsoft_teams/apps/http/http_server.py b/packages/apps/src/microsoft_teams/apps/http/http_server.py index 48b071c9..d1c5825c 100644 --- a/packages/apps/src/microsoft_teams/apps/http/http_server.py +++ b/packages/apps/src/microsoft_teams/apps/http/http_server.py @@ -79,7 +79,7 @@ def initialize( logger.debug("JWT validation enabled for %s", self._messaging_endpoint) elif not skip_auth: logger.warning( - "No credentials configured and skipAuth is not enabled. " + "No credentials configured and skip_auth is not enabled. " "All incoming requests will be rejected. Configure client authentication " "to securely receive messages, or set skip_auth=True for local development." ) From 50e6bb31af7af288aff369b7eec4a094ee193011 Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:46:24 -0700 Subject: [PATCH 3/4] Update test_http_server.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/apps/tests/test_http_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/tests/test_http_server.py b/packages/apps/tests/test_http_server.py index 651f0142..3357b220 100644 --- a/packages/apps/tests/test_http_server.py +++ b/packages/apps/tests/test_http_server.py @@ -132,7 +132,7 @@ async def test_handle_activity_no_handler(self, server): class TestHttpServerNoCredentials: - """Test cases for HttpServer when no credentials are configured and skipAuth is not set.""" + """Test cases for HttpServer when no credentials are configured and skip_auth is not set.""" @pytest.fixture def mock_adapter(self): From 74d73f54a2077d0e37f011312b7ef3cc4221c70b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 06:48:10 +0000 Subject: [PATCH 4/4] fix: use %-style formatting in logger warning call instead of f-string Agent-Logs-Url: https://github.com/microsoft/teams.py/sessions/0f5b7128-a158-4a68-b142-9f772f93cdd2 Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- packages/apps/src/microsoft_teams/apps/http/http_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/src/microsoft_teams/apps/http/http_server.py b/packages/apps/src/microsoft_teams/apps/http/http_server.py index d1c5825c..28f8f16e 100644 --- a/packages/apps/src/microsoft_teams/apps/http/http_server.py +++ b/packages/apps/src/microsoft_teams/apps/http/http_server.py @@ -129,7 +129,7 @@ async def handle_request(self, request: HttpRequest) -> HttpResponse: try: await self._token_validator.validate_token(raw_token, service_url) except Exception as e: - logger.warning(f"JWT token validation failed: {e}") + logger.warning("JWT token validation failed: %s", e) return HttpResponse(status=401, body={"error": "Unauthorized"}) token = cast(TokenProtocol, JsonWebToken(value=raw_token))