Securing your webhook endpoints is critical to ensure that webhook requests are coming from Gu1 and not from malicious actors. This guide covers how to verify webhook signatures, implement security best practices, and avoid common security mistakes.
Gu1 signs all webhook requests with an HMAC SHA-256 signature using your webhook secret. The signature is sent in the X-Webhook-Signature header, allowing you to verify that the request is authentic.
Gu1 generates a signature: When sending a webhook, Gu1 creates an HMAC SHA-256 hash of the raw request body using your webhook secret
Signature is sent in header: The signature is included in the X-Webhook-Signature header
Your server recalculates: Your endpoint recalculates the signature using the same secret and raw body
Compare signatures: If the signatures match, the webhook is authentic
Always verify webhook signatures in production. Without verification, anyone can send fake webhooks to your endpoint and potentially compromise your system.
Critical: You must verify the signature using the raw request body before it’s parsed as JSON. If you verify against the parsed JSON body (e.g., JSON.stringify(req.body)), the signature will not match because JSON formatting may differ.
from flask import Flask, request, jsonifyimport hmacimport hashlibimport osimport loggingapp = Flask(__name__)@app.route('/webhooks/gu1', methods=['POST'])def gu1_webhook(): try: # Get signature from header signature = request.headers.get('X-Webhook-Signature') webhook_secret = os.getenv('GU1_WEBHOOK_SECRET') # Get raw body for signature verification raw_body = request.get_data(as_text=True) # Verify signature if not verify_signature(raw_body, signature, webhook_secret): logging.error('Invalid webhook signature') return jsonify({'error': 'Invalid signature'}), 401 # Parse payload payload = request.json event = payload.get('event') data = payload.get('payload') logging.info(f'Received webhook: {event}') # Process the webhook handle_webhook(event, data) return jsonify({ 'success': True, 'message': 'Webhook received' }), 200 except Exception as e: logging.error(f'Webhook error: {e}') return jsonify({ 'success': False, 'error': str(e) }), 500def verify_signature(raw_body: str, signature: str, secret: str) -> bool: """Verify HMAC SHA-256 signature""" expected_signature = hmac.new( secret.encode('utf-8'), raw_body.encode('utf-8'), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected_signature)def handle_webhook(event: str, data: dict): # Process webhook based on event type passif __name__ == '__main__': app.run(port=3000)
Use hmac.compare_digest() instead of == for comparing signatures in Python. This function performs a timing-safe comparison that prevents timing attacks.
A common mistake is to verify the signature using the parsed JSON object instead of the raw request body. This will always fail because JSON formatting can differ.
Copy
Ask AI
// WRONG: Verifying parsed bodyapp.post('/webhooks', express.json(), (req, res) => { const signature = req.headers['x-webhook-signature']; // This will NOT work - JSON.stringify may format differently const body = JSON.stringify(req.body); const expectedSignature = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); if (signature === expectedSignature) { // Will always fail }});
Webhooks may be delivered more than once due to network issues, timeouts, or retries. Implement idempotency to ensure you process each webhook only once.
const Redis = require('ioredis');const redis = new Redis();async function handleWebhook(webhook) { const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`; // Try to set the key with NX (only if not exists) const result = await redis.set( `webhook:${webhookId}`, '1', 'EX', 86400, // Expire after 24 hours 'NX' ); if (!result) { console.log('Webhook already processed, skipping'); return; // Already processed } // Process webhook await processWebhookEvent(webhook);}
Python with Redis
Copy
Ask AI
import redisimport jsonredis_client = redis.Redis(host='localhost', port=6379, db=0)def handle_webhook(webhook): webhook_id = f"{webhook['payload']['entityId']}_{webhook['event']}_{webhook['timestamp']}" # Try to set the key with NX (only if not exists) result = redis_client.set( f"webhook:{webhook_id}", "1", ex=86400, # Expire after 24 hours nx=True ) if not result: print('Webhook already processed, skipping') return # Already processed # Process webhook process_webhook_event(webhook)
// DON'T DO THIS - No signature verificationapp.post('/webhooks', (req, res) => { // Processing webhook without verification handleWebhook(req.body); res.status(200).send('OK');});
Risk: Anyone can send fake webhooks to your endpoint.
// DON'T DO THIS - Processing duplicatesapp.post('/webhooks', (req, res) => { // No check for duplicates await createUser(req.body.entity); res.status(200).send('OK');});
Risk: Duplicate webhooks will create duplicate records.
// DON'T DO THIS - Processing unknown eventsapp.post('/webhooks', (req, res) => { // Accepting any event type handleAnyEvent(req.body.event, req.body.payload);});