Verificar firma de webhook
Snippets Node.js, Python, Go y PHP para validar HMAC SHA-256.
| Header | Descripción |
|---|---|
X-Soma9-Signature | sha256=<hex> del HMAC sobre ${timestamp}.${body} |
X-Soma9-Timestamp | Unix seconds — rechazar si difiere >5 min del clock actual |
X-Soma9-Event | Slug del evento (domain.registered, etc) |
Node.js
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/webhooks/soma9', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.headers['x-soma9-timestamp'];
const sig = req.headers['x-soma9-signature'];
const secret = process.env.SOMA9_WEBHOOK_SECRET;
// Anti-replay
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(401).send('timestamp out of range');
}
const expected =
'sha256=' +
crypto.createHmac('sha256', secret).update(`${ts}.${req.body}`).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
console.log('event:', event.event, event.data);
res.json({ received: true });
});Python
import hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['SOMA9_WEBHOOK_SECRET'].encode()
@app.post('/webhooks/soma9')
def receive():
raw = request.get_data()
ts = request.headers['X-Soma9-Timestamp']
sig = request.headers['X-Soma9-Signature']
if abs(time.time() - int(ts)) > 300:
abort(401, 'timestamp out of range')
expected = 'sha256=' + hmac.new(SECRET, f'{ts}.'.encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401, 'invalid signature')
event = request.get_json()
print('event:', event['event'], event['data'])
return {'received': True}Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"time"
)
func handle(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
ts := r.Header.Get("X-Soma9-Timestamp")
sig := r.Header.Get("X-Soma9-Signature")
tsInt, _ := strconv.ParseInt(ts, 10, 64)
if math.Abs(float64(time.Now().Unix()-tsInt)) > 300 {
http.Error(w, "timestamp out of range", 401)
return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("SOMA9_WEBHOOK_SECRET")))
mac.Write([]byte(ts + "." + string(body)))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(sig), []byte(expected)) {
http.Error(w, "invalid signature", 401)
return
}
// process event
}PHP
<?php
$secret = getenv('SOMA9_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_SOMA9_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_SOMA9_SIGNATURE'] ?? '';
if (abs(time() - (int)$ts) > 300) {
http_response_code(401); exit('timestamp out of range');
}
$expected = 'sha256=' . hash_hmac('sha256', "$ts.$raw", $secret);
if (!hash_equals($sig, $expected)) {
http_response_code(401); exit('invalid signature');
}
$event = json_decode($raw, true);
// process $event
echo json_encode(['received' => true]);Tip
Usa siempre timing-safe compare (hmac.compare_digest, crypto.timingSafeEqual,
hmac.Equal, hash_equals). Comparar strings con == expone tu app a timing attacks.