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