Verifying WhatsApp signature to ensure the webhook request is from WhatsApp.

This commit is contained in:
Filipi Fuchter
2025-10-06 16:16:59 -03:00
parent 084b133a01
commit 28929a47f7
3 changed files with 50 additions and 3 deletions

View File

@@ -158,3 +158,9 @@ NVIDIA_API_KEY=...
# Qwen # Qwen
QWEN_API_KEY=... 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: try:
import uvicorn import uvicorn
from dotenv import load_dotenv 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.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
except ImportError as e: except ImportError as e:
@@ -275,6 +275,7 @@ def _setup_whatsapp_routes(app: FastAPI):
WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN") WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN")
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID") WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN = os.getenv("WHATSAPP_WEBHOOK_VERIFICATION_TOKEN") WHATSAPP_WEBHOOK_VERIFICATION_TOKEN = os.getenv("WHATSAPP_WEBHOOK_VERIFICATION_TOKEN")
WHATSAPP_APP_SECRET = os.getenv("WHATSAPP_APP_SECRET")
if not all( if not all(
[ [
@@ -325,7 +326,12 @@ def _setup_whatsapp_routes(app: FastAPI):
summary="Handle WhatsApp webhook events", summary="Handle WhatsApp webhook events",
description="Processes incoming WhatsApp messages and call 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. """Handle incoming WhatsApp webhook events.
For call events, establishes WebRTC connections and spawns bot instances For call events, establishes WebRTC connections and spawns bot instances
@@ -357,7 +363,10 @@ def _setup_whatsapp_routes(app: FastAPI):
try: try:
# Process the webhook request # 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}") logger.debug(f"Webhook processed successfully: {result}")
return {"status": "success", "message": "Webhook processed successfully"} return {"status": "success", "message": "Webhook processed successfully"}
except ValueError as ve: except ValueError as ve:
@@ -376,6 +385,7 @@ def _setup_whatsapp_routes(app: FastAPI):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
whatsapp_client = WhatsAppClient( whatsapp_client = WhatsAppClient(
whatsapp_token=WHATSAPP_TOKEN, whatsapp_token=WHATSAPP_TOKEN,
whatsapp_secret=WHATSAPP_APP_SECRET,
phone_number_id=WHATSAPP_PHONE_NUMBER_ID, phone_number_id=WHATSAPP_PHONE_NUMBER_ID,
session=session, session=session,
) )

View File

@@ -12,6 +12,8 @@ WhatsApp call events.
""" """
import asyncio import asyncio
import hashlib
import hmac
from typing import Awaitable, Callable, Dict, List, Optional from typing import Awaitable, Callable, Dict, List, Optional
import aiohttp import aiohttp
@@ -47,6 +49,7 @@ class WhatsAppClient:
phone_number_id: str, phone_number_id: str,
session: aiohttp.ClientSession, session: aiohttp.ClientSession,
ice_servers: Optional[List[IceServer]] = None, ice_servers: Optional[List[IceServer]] = None,
whatsapp_secret: Optional[str] = None,
) -> None: ) -> None:
"""Initialize the WhatsApp client. """Initialize the WhatsApp client.
@@ -56,10 +59,12 @@ class WhatsAppClient:
session: aiohttp session for making HTTP requests session: aiohttp session for making HTTP requests
ice_servers: List of ICE servers for WebRTC connections. If None, ice_servers: List of ICE servers for WebRTC connections. If None,
defaults to Google's public STUN server 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( self._whatsapp_api = WhatsAppApi(
whatsapp_token=whatsapp_token, phone_number_id=phone_number_id, session=session whatsapp_token=whatsapp_token, phone_number_id=phone_number_id, session=session
) )
self._whatsapp_secret = whatsapp_secret
self._ongoing_calls_map: Dict[str, SmallWebRTCConnection] = {} self._ongoing_calls_map: Dict[str, SmallWebRTCConnection] = {}
# Set default ICE servers if none provided # Set default ICE servers if none provided
@@ -133,10 +138,32 @@ class WhatsAppClient:
return int(challenge) 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( async def handle_webhook_request(
self, self,
request: WhatsAppWebhookRequest, request: WhatsAppWebhookRequest,
connection_callback: Optional[Callable[[SmallWebRTCConnection], Awaitable[None]]] = None, connection_callback: Optional[Callable[[SmallWebRTCConnection], Awaitable[None]]] = None,
raw_body: Optional[bytes] = None,
sha256_signature: Optional[str] = None,
) -> bool: ) -> bool:
"""Handle a webhook request from WhatsApp. """Handle a webhook request from WhatsApp.
@@ -150,6 +177,8 @@ class WhatsAppClient:
connection_callback: Optional callback function to invoke when a new connection_callback: Optional callback function to invoke when a new
WebRTC connection is established. The callback WebRTC connection is established. The callback
receives the SmallWebRTCConnection instance. 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: Returns:
bool: True if the webhook request was handled successfully, False otherwise 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 Exception: If connection establishment or API calls fail
""" """
try: try:
if self._whatsapp_secret:
await self._validate_whatsapp_webhook_request(raw_body, sha256_signature)
for entry in request.entry: for entry in request.entry:
for change in entry.changes: for change in entry.changes:
# Handle connect events # Handle connect events