Hoppa till innehåll

Verifieringsexempel

Varje exempel gör samma fem saker som beskrivs under Webhooks: aktualitet, signaturkontroll i konstant tid, nonce, tolkning och borttagning av dubbletter utifrån händelse-ID. Nonce-lagret visas som Redis; vilket lager som helst med en atomisk operation som bara sätter värdet om det saknas och en TTL fungerar.

import hmac, hashlib, json, time
from flask import Flask, request, abort
import redis
app = Flask(__name__)
r = redis.Redis()
SECRET = os.environ["ROLCHAT_WEBHOOK_SECRET"].encode()
@app.post("/rolchat/webhook")
def webhook():
ts = request.headers.get("X-RolChat-Timestamp", "")
sig = request.headers.get("X-RolChat-Signature", "")
nonce = request.headers.get("X-RolChat-Nonce", "")
event_id = request.headers.get("X-RolChat-Event-Id", "")
body = request.get_data() # raw bytes
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(400, "timestamp_out_of_window")
expected = "sha256=" + hmac.new(SECRET, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401, "invalid_signature")
if not r.set(f"webhook_nonce:{nonce}", "1", nx=True, ex=600):
abort(409, "duplicate_nonce")
event = json.loads(body)
if not r.set(f"webhook_event:{event_id}", "1", nx=True, ex=7 * 24 * 3600):
return "", 200 # already processed
enqueue(event) # do the work off the request thread
return "", 200
import crypto from 'node:crypto';
import express from 'express';
import Redis from 'ioredis';
const app = express();
const redis = new Redis();
const SECRET = process.env.ROLCHAT_WEBHOOK_SECRET;
// Keep the raw body: express.json() would re-serialise it.
app.post('/rolchat/webhook', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
const ts = req.get('X-RolChat-Timestamp') ?? '';
const sig = req.get('X-RolChat-Signature') ?? '';
const nonce = req.get('X-RolChat-Nonce') ?? '';
const eventId = req.get('X-RolChat-Event-Id') ?? '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send('timestamp_out_of_window');
}
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
.update(`${ts}.`).update(req.body).digest('hex');
const a = Buffer.from(sig), b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('invalid_signature');
}
if (!(await redis.set(`webhook_nonce:${nonce}`, '1', 'EX', 600, 'NX'))) {
return res.status(409).send('duplicate_nonce');
}
const event = JSON.parse(req.body.toString('utf8'));
if (!(await redis.set(`webhook_event:${eventId}`, '1', 'EX', 7 * 86400, 'NX'))) {
return res.sendStatus(200); // already processed
}
await queue.add('rolchat-event', event);
res.sendStatus(200);
});
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"math"
"net/http"
"strconv"
"time"
)
var secret = []byte(os.Getenv("ROLCHAT_WEBHOOK_SECRET"))
func webhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
ts := r.Header.Get("X-RolChat-Timestamp")
sig := r.Header.Get("X-RolChat-Signature")
nonce := r.Header.Get("X-RolChat-Nonce")
eventID := r.Header.Get("X-RolChat-Event-Id")
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 {
http.Error(w, "timestamp_out_of_window", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(sig), []byte(expected)) {
http.Error(w, "invalid_signature", http.StatusUnauthorized)
return
}
// SET NX EX 600 - any store with an atomic set-if-absent works.
if !nonceStore.SetNX("webhook_nonce:"+nonce, 10*time.Minute) {
http.Error(w, "duplicate_nonce", http.StatusConflict)
return
}
if !nonceStore.SetNX("webhook_event:"+eventID, 7*24*time.Hour) {
w.WriteHeader(http.StatusOK) // already processed
return
}
go process(body)
w.WriteHeader(http.StatusOK)
}
require "openssl"
require "json"
SECRET = ENV.fetch("ROLCHAT_WEBHOOK_SECRET")
def rolchat_webhook(request)
body = request.raw_post # raw bytes, not params
ts = request.headers["X-RolChat-Timestamp"].to_s
sig = request.headers["X-RolChat-Signature"].to_s
nonce = request.headers["X-RolChat-Nonce"].to_s
event_id = request.headers["X-RolChat-Event-Id"].to_s
return [400, "timestamp_out_of_window"] unless ts =~ /\A\d+\z/ && (Time.now.to_i - ts.to_i).abs <= 300
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", SECRET, "#{ts}.#{body}")
return [401, "invalid_signature"] unless ActiveSupport::SecurityUtils.secure_compare(sig, expected)
return [409, "duplicate_nonce"] unless REDIS.set("webhook_nonce:#{nonce}", "1", nx: true, ex: 600)
event = JSON.parse(body)
return [200, ""] unless REDIS.set("webhook_event:#{event_id}", "1", nx: true, ex: 7 * 86_400)
RolchatEventJob.perform_later(event)
[200, ""]
end
<?php
$secret = getenv('ROLCHAT_WEBHOOK_SECRET');
$body = file_get_contents('php://input'); // raw bytes
$ts = $_SERVER['HTTP_X_ROLCHAT_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_ROLCHAT_SIGNATURE'] ?? '';
$nonce = $_SERVER['HTTP_X_ROLCHAT_NONCE'] ?? '';
$eventId = $_SERVER['HTTP_X_ROLCHAT_EVENT_ID'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) {
http_response_code(400); exit('timestamp_out_of_window');
}
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401); exit('invalid_signature');
}
$redis = new Redis(); $redis->connect('127.0.0.1');
if (!$redis->set("webhook_nonce:$nonce", '1', ['nx', 'ex' => 600])) {
http_response_code(409); exit('duplicate_nonce');
}
$event = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!$redis->set("webhook_event:$eventId", '1', ['nx', 'ex' => 7 * 86400])) {
http_response_code(200); exit; // already processed
}
enqueue($event);
http_response_code(200);

Be RolChat skicka en syntetisk signerad händelse till din endpoint:

POST /api/v1/webhooks/test-signature
Authorization: Bearer pat_…
X-Account-Id: <account uuid>

Om din mottagare svarar 200 på det anropet och 401 när du ändrar en byte i bodyn är den korrekt.