Verifying signatures
Every webhook POST Aigeon sends includes an X-Aigeon-Signature header. Verifying it before processing the payload protects you from spoofed requests.
Header format
X-Aigeon-Signature: t=1745000000,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
t— the Unix timestamp Aigeon used when computing the signature. Use it for replay-attack prevention.v1— HMAC-SHA256 of<t>.<raw_body>using your signing secret as the key.
Verification algorithm
- Parse
tandv1from the header. - Reject the request if
tis more than 5 minutes in the past (replay protection). - Compute
expected = HMAC-SHA256(key=signing_secret, msg="<t>.<raw_body>"). - Use a constant-time comparison between
expectedandv1.
Node.js example
import { createHmac, timingSafeEqual } from "crypto";
function verifyAigeonSignature(
rawBody: Buffer,
sigHeader: string,
secret: string,
): boolean {
const parts = Object.fromEntries(
sigHeader.split(",").map((p) => p.split("=")),
);
const t = parts["t"];
const v1 = parts["v1"];
if (!t || !v1) return false;
// Replay attack protection: reject anything older than 5 minutes
if (Math.abs(Date.now() / 1000 - parseInt(t, 10)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.`)
.update(rawBody)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Express usage:
app.post("/aigeon-hook", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyAigeonSignature(
req.body,
req.headers["x-aigeon-signature"] as string,
process.env.AIGEON_WEBHOOK_SECRET!,
);
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString());
// … handle event
res.sendStatus(200);
});
Use
express.raw()— notexpress.json()— so thatreq.bodyis the raw Buffer before JSON parsing. Running the body through a JSON parser first changes whitespace and breaks the HMAC.
Python example
import hashlib, hmac, time
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = os.environ["AIGEON_WEBHOOK_SECRET"]
@app.post("/aigeon-hook")
async def handle_hook(request: Request):
raw_body = await request.body()
sig_header = request.headers.get("x-aigeon-signature", "")
parts = dict(p.split("=", 1) for p in sig_header.split(",") if "=" in p)
t = parts.get("t", "")
v1 = parts.get("v1", "")
if abs(time.time() - int(t)) > 300:
raise HTTPException(403, "Timestamp too old")
expected = hmac.new(
WEBHOOK_SECRET.encode(),
f"{t}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, v1):
raise HTTPException(403, "Invalid signature")
event = json.loads(raw_body)
# … handle event
return {"ok": True}
