Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions packages/apps/src/microsoft_teams/apps/http/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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."
)

self._adapter.register_route("POST", self._messaging_endpoint, self.handle_request)
self._initialized = True
Expand All @@ -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 ")
if self._skip_auth:
# Auth explicitly skipped — use a default token
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
service_url = cast(Optional[str], body.get("serviceUrl"))
token = cast(
token: TokenProtocol = cast(
TokenProtocol,
SimpleNamespace(
app_id="",
Expand All @@ -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."
Comment on lines +116 to +119
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will emit an error on every request, while the TS instead logs a warning on startup and then after that returns 401 silently. Should we align behavior?

)
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("JWT token validation failed: %s", e)
return HttpResponse(status=401, body={"error": "Unauthorized"})
Comment on lines +131 to +133
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid f-strings in logger calls; they eagerly format even when the log level is disabled. Use logger's %-style formatting (or pass exc_info) so formatting is lazy and exception details can be captured consistently.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can you address?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 74d73f5 — changed logger.warning(f"JWT token validation failed: {e}") to logger.warning("JWT token validation failed: %s", e) so the message is only formatted when the warning level is actually enabled.


token = cast(TokenProtocol, JsonWebToken(value=raw_token))

core_activity = CoreActivity.model_validate(body)
activity_type = core_activity.type or "unknown"
Expand Down
60 changes: 59 additions & 1 deletion packages/apps/tests/test_http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 skip_auth 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."""

Expand All @@ -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)
Expand Down
Loading