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:
@@ -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=
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user