diff --git a/README.md b/README.md index 3b6f6afc..e1e58c87 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,7 @@ communication_services: endpoint: https://xxx.france.communication.azure.com phone_number: "+33612345678" post_queue_name: post-33612345678 + resource_id: xxx sms_queue_name: sms-33612345678 cognitive_service: diff --git a/function_app.py b/function_app.py index 93f2302f..b426e2f3 100644 --- a/function_app.py +++ b/function_app.py @@ -2,11 +2,12 @@ import json from http import HTTPStatus from os import getenv -from typing import Any, Optional +from typing import Any, Optional, Union from urllib.parse import quote_plus, urljoin from uuid import UUID import azure.functions as func +import jwt import mistune from azure.communication.callautomation import PhoneNumberIdentifier from azure.communication.callautomation.aio import CallAutomationClient @@ -66,6 +67,10 @@ _automation_client: Optional[CallAutomationClient] = None _source_caller = PhoneNumberIdentifier(CONFIG.communication_services.phone_number) logger.info("Using phone number %s", CONFIG.communication_services.phone_number) +_communication_services_jwks_client = jwt.PyJWKClient( + cache_keys=True, + uri="https://acscallautomation.communication.azure.com/calling/keys", +) # Persistences _cache = CONFIG.cache.instance() @@ -471,8 +476,28 @@ async def communicationservices_event_post( No parameters are expected. The body is a list of JSON objects `CloudEvent`. - Returns a 204 No Content if the events are properly fomatted. Otherwise, returns a 400 Bad Request. + Returns a 204 No Content if the events are properly fomatted. A 401 Unauthorized if the JWT token is invalid. Otherwise, returns a 400 Bad Request. """ + # Validate JWT token + service_jwt: Union[str, None] = req.headers.get("Authorization") + if not service_jwt: + return func.HttpResponse(status_code=HTTPStatus.UNAUTHORIZED) + service_jwt = str(service_jwt).replace("Bearer ", "") + try: + jwt.decode( + algorithms=["RS256"], + audience=CONFIG.communication_services.resource_id, + issuer="https://acscallautomation.communication.azure.com", + jwt=service_jwt, + key=_communication_services_jwks_client.get_signing_key_from_jwt( + service_jwt + ).key, + ) + except jwt.PyJWTError: + logger.warning("Invalid JWT token", exc_info=True) + return func.HttpResponse(status_code=HTTPStatus.UNAUTHORIZED) + + # Validate request try: call_id = UUID(req.route_params["call_id"]) secret: str = req.route_params["secret"] @@ -484,6 +509,8 @@ async def communicationservices_event_post( return _validation_error(Exception("Invalid JSON format")) if not events or not isinstance(events, list): return _validation_error(Exception("Events must be a list")) + + # Process events in parallel await asyncio.gather( *[ _communicationservices_event_worker( @@ -496,6 +523,8 @@ async def communicationservices_event_post( for event in events ] ) + + # Return default response return func.HttpResponse(status_code=HTTPStatus.NO_CONTENT) diff --git a/helpers/config_models/communication_services.py b/helpers/config_models/communication_services.py index e0f973cd..79fe49e3 100644 --- a/helpers/config_models/communication_services.py +++ b/helpers/config_models/communication_services.py @@ -9,5 +9,6 @@ class CommunicationServicesModel(BaseModel): endpoint: str phone_number: PhoneNumber post_queue_name: str + resource_id: str sms_queue_name: str trainings_queue_name: str diff --git a/pyproject.toml b/pyproject.toml index 15ea6254..26950387 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "pydantic-extra-types==2.8.2", # Extra types for Pydantic "pydantic-settings==2.3.3", # Application configuration management with Pydantic "pydantic[email]==2.7.4", # Data serialization and validation, plus email validation + "pyjwt==2.8.0", # Secure inbound calls from Communication Services "python-dotenv==1.0.1", # Load environment variables from .env file "pytz==2024.1", # Time zone handling "pyyaml==6.0.1", # YAML parser diff --git a/requirements-dev.txt b/requirements-dev.txt index b9ff06b4..35a7a40e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1702,6 +1702,7 @@ pyjwt[crypto]==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via + # call-center-ai (pyproject.toml) # msal # twilio pylint==3.2.5 \ diff --git a/requirements.txt b/requirements.txt index b6592ed9..69450134 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1153,6 +1153,7 @@ pyjwt[crypto]==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via + # call-center-ai (pyproject.toml) # msal # twilio python-dotenv==1.0.1 \