Merge pull request #2794 from pipecat-ai/filipi/verifying_whatsapp_signature

Verifying WhatsApp signature to ensure the webhook request is from WhatsApp.
This commit is contained in:
Filipi da Silva Fuchter
2025-10-07 08:57:47 -03:00
committed by GitHub
3 changed files with 50 additions and 3 deletions

View File

@@ -158,3 +158,9 @@ NVIDIA_API_KEY=...
# Qwen
QWEN_API_KEY=...
# WhatsApp
WHATSAPP_TOKEN=
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN=
WHATSAPP_PHONE_NUMBER_ID=
WHATSAPP_APP_SECRET=

View File

@@ -84,7 +84,7 @@ from pipecat.runner.types import (
try:
import uvicorn
from dotenv import load_dotenv
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request, WebSocket
from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, RedirectResponse
except ImportError as e:
@@ -275,6 +275,7 @@ def _setup_whatsapp_routes(app: FastAPI):
WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN")
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN = os.getenv("WHATSAPP_WEBHOOK_VERIFICATION_TOKEN")
WHATSAPP_APP_SECRET = os.getenv("WHATSAPP_APP_SECRET")
if not all(
[
@@ -325,7 +326,12 @@ def _setup_whatsapp_routes(app: FastAPI):
summary="Handle WhatsApp webhook events",
description="Processes incoming WhatsApp messages and call events",
)
async def whatsapp_webhook(body: WhatsAppWebhookRequest, background_tasks: BackgroundTasks):
async def whatsapp_webhook(
body: WhatsAppWebhookRequest,
background_tasks: BackgroundTasks,
request: Request,
x_hub_signature_256: str = Header(None),
):
"""Handle incoming WhatsApp webhook events.
For call events, establishes WebRTC connections and spawns bot instances
@@ -357,7 +363,10 @@ def _setup_whatsapp_routes(app: FastAPI):
try:
# Process the webhook request
result = await whatsapp_client.handle_webhook_request(body, connection_callback)
raw_body = await request.body()
result = await whatsapp_client.handle_webhook_request(
body, connection_callback, sha256_signature=x_hub_signature_256, raw_body=raw_body
)
logger.debug(f"Webhook processed successfully: {result}")
return {"status": "success", "message": "Webhook processed successfully"}
except ValueError as ve:
@@ -376,6 +385,7 @@ def _setup_whatsapp_routes(app: FastAPI):
async with aiohttp.ClientSession() as session:
whatsapp_client = WhatsAppClient(
whatsapp_token=WHATSAPP_TOKEN,
whatsapp_secret=WHATSAPP_APP_SECRET,
phone_number_id=WHATSAPP_PHONE_NUMBER_ID,
session=session,
)

View File

@@ -12,6 +12,8 @@ WhatsApp call events.
"""
import asyncio
import hashlib
import hmac
from typing import Awaitable, Callable, Dict, List, Optional
import aiohttp
@@ -47,6 +49,7 @@ class WhatsAppClient:
phone_number_id: str,
session: aiohttp.ClientSession,
ice_servers: Optional[List[IceServer]] = None,
whatsapp_secret: Optional[str] = None,
) -> None:
"""Initialize the WhatsApp client.
@@ -56,10 +59,12 @@ class WhatsAppClient:
session: aiohttp session for making HTTP requests
ice_servers: List of ICE servers for WebRTC connections. If None,
defaults to Google's public STUN server
whatsapp_secret: WhatsApp APP secret for validating that the webhook request came from WhatsApp.
"""
self._whatsapp_api = WhatsAppApi(
whatsapp_token=whatsapp_token, phone_number_id=phone_number_id, session=session
)
self._whatsapp_secret = whatsapp_secret
self._ongoing_calls_map: Dict[str, SmallWebRTCConnection] = {}
# Set default ICE servers if none provided
@@ -133,10 +138,32 @@ class WhatsAppClient:
return int(challenge)
async def _validate_whatsapp_webhook_request(self, raw_body: bytes, sha256_signature: str):
"""Common handler for both /start and /connect endpoints."""
# Compute HMAC SHA256 using your App Secret
expected_signature = hmac.new(
key=self._whatsapp_secret.encode("utf-8"),
msg=raw_body,
digestmod=hashlib.sha256,
).hexdigest()
# Extract signature from header (strip 'sha256=' prefix)
if not sha256_signature:
raise Exception("Missing X-Hub-Signature-256 header")
received_signature = sha256_signature.split("sha256=")[-1]
# Compare signatures securely
if not hmac.compare_digest(expected_signature, received_signature):
raise Exception("Invalid webhook signature")
logger.debug(f"Webhook signature verified!")
async def handle_webhook_request(
self,
request: WhatsAppWebhookRequest,
connection_callback: Optional[Callable[[SmallWebRTCConnection], Awaitable[None]]] = None,
raw_body: Optional[bytes] = None,
sha256_signature: Optional[str] = None,
) -> bool:
"""Handle a webhook request from WhatsApp.
@@ -150,6 +177,8 @@ class WhatsAppClient:
connection_callback: Optional callback function to invoke when a new
WebRTC connection is established. The callback
receives the SmallWebRTCConnection instance.
raw_body: Optional bytes containing the raw request body.
sha256_signature: Optional X-Hub-Signature-256 header value from the request.
Returns:
bool: True if the webhook request was handled successfully, False otherwise
@@ -159,6 +188,8 @@ class WhatsAppClient:
Exception: If connection establishment or API calls fail
"""
try:
if self._whatsapp_secret:
await self._validate_whatsapp_webhook_request(raw_body, sha256_signature)
for entry in request.entry:
for change in entry.changes:
# Handle connect events