11.5Code examples
Each example reads the raw body, checks the signature with a constant-time comparison, and replies at once. Store the secret in the environment variable OBSRVLY_WEBHOOK_SECRET.
Node.js (Express)#
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.OBSRVLY_WEBHOOK_SECRET;
const app = express();
// express.raw keeps the exact bytes. Do not use express.json here.
app.post(
"/alerts",
express.raw({ type: "application/json" }),
(req, res) => {
const header = req.get("X-Obsrvly-Signature") || "";
if (!header.startsWith("sha256=")) return res.sendStatus(401);
const sent = Buffer.from(header.slice(7), "hex");
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.body)
.digest();
if (
sent.length !== expected.length ||
!crypto.timingSafeEqual(sent, expected)
) {
return res.sendStatus(401);
}
res.sendStatus(204); // reply first
const alert = JSON.parse(req.body.toString("utf8"));
console.log(alert.event, alert.alert_id, alert.monitor_name);
},
);
app.listen(3000);Python (Flask)#
import hashlib
import hmac
import os
from flask import Flask, abort, request
SECRET = os.environ["OBSRVLY_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/alerts")
def alerts():
raw = request.get_data() # raw bytes, before any JSON parsing
header = request.headers.get("X-Obsrvly-Signature", "")
digest = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
expected = "sha256=" + digest
if not hmac.compare_digest(header.encode(), expected.encode()):
abort(401)
alert = request.get_json()
print(alert["event"], alert["alert_id"], alert.get("monitor_name"))
return "", 204Go#
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strings"
)
var secret = []byte(os.Getenv("OBSRVLY_WEBHOOK_SECRET"))
func alerts(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad body", http.StatusBadRequest)
return
}
header := r.Header.Get("X-Obsrvly-Signature")
if !strings.HasPrefix(header, "sha256=") {
http.Error(w, "no signature", http.StatusUnauthorized)
return
}
sent, err := hex.DecodeString(strings.TrimPrefix(header, "sha256="))
mac := hmac.New(sha256.New, secret)
mac.Write(raw)
if err != nil || !hmac.Equal(sent, mac.Sum(nil)) {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
var alert map[string]any
if err := json.Unmarshal(raw, &alert); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
log.Println(alert["event"], alert["alert_id"], alert["monitor_name"])
}
func main() {
http.HandleFunc("/alerts", alerts)
log.Fatal(http.ListenAndServe(":3000", nil))
}Common mistakes#
- Parsing the JSON first. Parsing and re-encoding the body changes its bytes, so the signature no longer matches. Check the raw body.
- Comparing with ==. Use a constant-time comparison, as in the examples.
- Using the wrong secret. Use the channel's HMAC signing secret, not an API key.
- Leaving out sha256=. The header value is
sha256=followed by the hex signature. - Extra spaces in the secret. obsrvly uses the secret exactly as typed. Make sure your copy matches.