Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 skipAuth is not enabled. "
Comment thread
heyitsaamir marked this conversation as resolved.
Outdated
"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 ")
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="",
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 thread
corinagum marked this conversation as resolved.
Outdated
)
Comment thread
heyitsaamir marked this conversation as resolved.
Outdated
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"})
Comment thread
corinagum marked this conversation as resolved.

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 skipAuth is not set."""
Comment thread
heyitsaamir marked this conversation as resolved.
Outdated

@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