Webhook Signature Verification
Learn how to verify webhook authenticity using HMAC-SHA256 signatures to secure your integration.
Why Verify Signatures?
Without signature verification, anyone could send fake webhook requests to your server, potentially:
- Crediting accounts without payment
- Triggering fraudulent order fulfillment
- Manipulating your business logic
Always verify signatures to ensure requests genuinely came from BlockEden.xyz.
How It Works
BlockEden.xyz signs every webhook request using HMAC-SHA256:
signature = HMAC-SHA256(your_webhook_secret, raw_request_body)
The signature is sent in the x-eden-signature HTTP header. Your server must:
- Read the raw request body (before parsing)
- Compute the expected signature using your secret
- Compare it with the received signature
- Only process the event if signatures match
Implementation Guide
Node.js with Express
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifyWebhookSignature(body, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// IMPORTANT: Use express.raw() to get the raw body
app.post('/api/webhooks',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-eden-signature'];
if (!signature) {
console.error('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Missing signature header');
return res.status(401).send('Missing signature');
}
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
console.error('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Invalid signature');
return res.status(401).send('Invalid signature');
}
// Signature valid - safe to process
const event = JSON.parse(req.body.toString());
console.log('<ion-icon name="checkmark-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Signature verified for event:', event.type);
// Process event...
res.status(200).json({ received: true });
}
);
app.listen(3000);
Next.js App Router
// app/api/webhooks/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
function verifySignature(body: string, signature: string): boolean {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
export async function POST(req: NextRequest) {
const signature = req.headers.get('x-eden-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
);
}
// Get raw body text for verification
const body = await req.text();
if (!verifySignature(body, signature)) {
console.error('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Invalid signature');
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Signature valid - safe to process
const event = JSON.parse(body);
console.log('<ion-icon name="checkmark-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Signature verified:', event.type);
// Process event...
return NextResponse.json({ received: true });
}
Python with Flask
import hmac
import hashlib
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
"""Verify webhook signature using constant-time comparison."""
expected_signature = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
# Use compare_digest for constant-time comparison
return hmac.compare_digest(signature, expected_signature)
@app.route('/api/webhooks', methods=['POST'])
def webhook():
signature = request.headers.get('x-eden-signature')
if not signature:
return jsonify({'error': 'Missing signature'}), 401
# Get raw body bytes (before parsing)
body = request.get_data()
if not verify_webhook_signature(body, signature, WEBHOOK_SECRET):
print('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Invalid signature')
return jsonify({'error': 'Invalid signature'}), 401
# Signature valid - safe to process
event = request.get_json()
print(f'<ion-icon name="checkmark-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Signature verified: {event["type"]}')
# Process event...
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
Python with FastAPI
import hmac
import hashlib
import os
from fastapi import FastAPI, Request, HTTPException, Header
from typing import Optional
app = FastAPI()
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
"""Verify webhook signature using constant-time comparison."""
expected_signature = hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
@app.post("/api/webhooks")
async def webhook(
request: Request,
x_eden_signature: Optional[str] = Header(None)
):
if not x_eden_signature:
raise HTTPException(status_code=401, detail="Missing signature")
# Get raw body bytes
body = await request.body()
if not verify_webhook_signature(body, x_eden_signature, WEBHOOK_SECRET):
print('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Invalid signature')
raise HTTPException(status_code=401, detail="Invalid signature")
# Signature valid - safe to process
event = await request.json()
print(f'<ion-icon name="checkmark-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Signature verified: {event["type"]}')
# Process event...
return {"received": True}
Ruby with Sinatra
require 'sinatra'
require 'openssl'
require 'json'
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']
def verify_webhook_signature(body, signature, secret)
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
Rack::Utils.secure_compare(signature, expected_signature)
end
post '/api/webhooks' do
signature = request.env['HTTP_X_EDEN_SIGNATURE']
unless signature
halt 401, JSON.generate({ error: 'Missing signature' })
end
# Get raw body
request.body.rewind
body = request.body.read
unless verify_webhook_signature(body, signature, WEBHOOK_SECRET)
puts '<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Invalid signature'
halt 401, JSON.generate({ error: 'Invalid signature' })
end
# Signature valid - safe to process
event = JSON.parse(body)
puts "<ion-icon name=\"checkmark-circle\" style={{verticalAlign: \"middle\", fontSize: \"1.2em\", color: \"#10b981\"}}></ion-icon> Signature verified: #{event['type']}"
# Process event...
status 200
JSON.generate({ received: true })
end
PHP
<?php
function verifyWebhookSignature($body, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $body, $secret);
// Use hash_equals for constant-time comparison
return hash_equals($signature, $expectedSignature);
}
// Get raw POST body
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_EDEN_SIGNATURE'] ?? '';
$secret = getenv('WEBHOOK_SECRET');
if (!$signature) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => 'Missing signature']);
exit;
}
if (!verifyWebhookSignature($body, $signature, $secret)) {
error_log('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Invalid signature');
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Signature valid - safe to process
$event = json_decode($body, true);
error_log("<ion-icon name=\"checkmark-circle\" style={{verticalAlign: \"middle\", fontSize: \"1.2em\", color: \"#10b981\"}}></ion-icon> Signature verified: {$event['type']}");
// Process event...
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);
?>
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
)
var webhookSecret = os.Getenv("WEBHOOK_SECRET")
func verifyWebhookSignature(body []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// Use constant-time comparison
return subtle.ConstantTimeCompare(
[]byte(signature),
[]byte(expectedSignature),
) == 1
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("x-eden-signature")
if signature == "" {
http.Error(w, `{"error":"Missing signature"}`, http.StatusUnauthorized)
return
}
// Read raw body
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, `{"error":"Error reading body"}`, http.StatusBadRequest)
return
}
if !verifyWebhookSignature(body, signature, webhookSecret) {
log.Println("<ion-icon name=\"close-circle\" style={{verticalAlign: \"middle\", fontSize: \"1.2em\", color: \"#ef4444\"}}></ion-icon> Invalid signature")
http.Error(w, `{"error":"Invalid signature"}`, http.StatusUnauthorized)
return
}
// Signature valid - safe to process
var event map[string]interface{}
json.Unmarshal(body, &event)
log.Printf("<ion-icon name=\"checkmark-circle\" style={{verticalAlign: \"middle\", fontSize: \"1.2em\", color: \"#10b981\"}}></ion-icon> Signature verified: %s\n", event["type"])
// Process event...
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received":true}`))
}
func main() {
http.HandleFunc("/api/webhooks", webhookHandler)
fmt.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}