Facebook Webhook Signature Validation Fix
Problem
When testing the Facebook webhook from the Facebook Developer Console, the signature validation was failing with "Invalid signature" error:
Invalid signature
Invalid signature
Invalid signature
HTTP 401 Unauthorized responses were being returned.
Root Cause
The issue was that MedusaJS's default JSON body parser was consuming and parsing the request body before our webhook handler could access it. Facebook's signature validation requires the exact raw body that was sent in the request to calculate the HMAC-SHA256 signature.
When we used JSON.stringify(req.body) to recreate the raw body, it wouldn't match Facebook's original payload because:
- JSON stringification order might differ
- Whitespace might differ
- The parsed object loses the exact formatting
Solution
1. Disable Body Parser for Webhook Route
In /src/api/middlewares.ts, we disabled the default JSON body parser for the Facebook webhook route:
{
matcher: "/webhooks/social/facebook",
method: "POST",
middlewares: [],
bodyParser: false, // Disable default JSON body parser for this route
},
2. Manually Read and Parse Raw Body
In /src/api/webhooks/social/facebook/route.ts, we manually read the raw body before parsing:
// Read raw body for signature validation
let rawBody = '';
// Collect raw body chunks
await new Promise<void>((resolve, reject) => {
req.on('data', (chunk) => {
rawBody += chunk.toString('utf8');
});
req.on('end', () => resolve());
req.on('error', (err) => reject(err));
});
// Calculate expected signature using the raw body
const expectedSignature = crypto
.createHmac("sha256", appSecret)
.update(rawBody, 'utf8')
.digest("hex")
const isValid = `sha256=${expectedSignature}` === signature
// Only parse the body after signature validation
const body = JSON.parse(rawBody) as FacebookWebhookPayload
How Facebook Signature Validation Works
-
Facebook sends webhook with signature:
POST /webhooks/social/facebook
X-Hub-Signature-256: sha256=abc123...
Content-Type: application/json
{"object":"page","entry":[...]} -
Server calculates expected signature:
const expectedSignature = crypto
.createHmac("sha256", APP_SECRET)
.update(rawBody, 'utf8')
.digest("hex") -
Compare signatures:
const isValid = `sha256=${expectedSignature}` === receivedSignature
Testing
Before Fix:
curl -X POST https://your-domain.com/webhooks/social/facebook \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=..." \
-d '{"object":"page","entry":[]}'
# Response: 401 Unauthorized
# Error: "Invalid signature"
After Fix:
# Same request
# Response: 200 OK
# Body: "EVENT_RECEIVED"
Test from Facebook Developer Console:
- Go to: https://developers.facebook.com/apps/YOUR_APP_ID/webhooks/
- Click "Test" button next to your webhook subscription
- Select event type (e.g., "feed")
- Click "Send to My Server"
- Expected: Status 200, Response: "EVENT_RECEIVED"
Debug Logging
The fix includes enhanced debug logging for signature validation failures:
if (!isValid) {
console.error("Invalid signature", {
received: signature,
expected: `sha256=${expectedSignature}`,
bodyLength: rawBody.length,
bodyPreview: rawBody.substring(0, 100)
})
return res.status(401).send("Unauthorized")
}
This helps diagnose issues by showing:
- The signature Facebook sent
- The signature we calculated
- The length of the raw body
- A preview of the raw body content
Important Notes
-
Raw Body is Critical: Never use
JSON.stringify(req.body)for signature validation. Always use the original raw body. -
UTF-8 Encoding: Ensure consistent UTF-8 encoding when reading the body and calculating the signature.
-
Body Parser Disabled: The
bodyParser: falsesetting only affects this specific route. Other routes continue to use the default JSON body parser. -
Performance: Reading the raw body manually adds minimal overhead and is necessary for security.
Security Implications
Proper signature validation ensures:
- ✅ Requests actually come from Facebook
- ✅ Payloads haven't been tampered with
- ✅ Protection against replay attacks (when combined with timestamp validation)
- ✅ No unauthorized access to webhook endpoint
Files Modified
/src/api/middlewares.ts- Disabled body parser for webhook route/src/api/webhooks/social/facebook/route.ts- Manual raw body reading and parsing