Security & Auth
Plain-English guide to verifying ConnectSafely webhook signatures so attackers can't fake events. Includes ready-to-run Node.js and Python examples.
Why this matters
The URL you gave ConnectSafely is public. Anyone who guesses or finds it can hit it and pretend to be us. If your code blindly trusts every request, an attacker could:
- Fake a "new message" with text that triggers your auto-reply logic
- Inject malicious data into your CRM
- Hammer you with bogus events
The fix: don't trust the request unless we signed it. Every real ConnectSafely delivery carries a cryptographic signature that proves it came from us. If the signature is missing or wrong, reject the request.
How signing works (the 30-second version)
- When you create a webhook, ConnectSafely generates a random signing secret (a long random string) and shows it to you once.
- For every event we send you, we hash the request body together with that secret to produce a short fingerprint, and put it in the
X-Webhook-Signatureheader. - On your side, you do the exact same hash with the secret you saved, and compare. If they match, the request is real. If they don't, drop it.
The hash algorithm is HMAC-SHA256. The header value looks like:
X-Webhook-Signature: sha256=9a3c8e...0bOnly the hex after sha256= is the actual signature.
Verify it — Node.js (Express)
import express from "express";
import crypto from "crypto";
const app = express();
const SECRET = process.env.CONNECTSAFELY_WEBHOOK_SECRET!; // from the dashboard
app.post(
"/webhook",
// IMPORTANT: read the RAW body (not JSON-parsed) for verification.
express.raw({ type: "application/json" }),
(req, res) => {
const sent = req.header("X-Webhook-Signature") || "";
const expected =
"sha256=" +
crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
// Constant-time compare to avoid timing attacks.
const a = Buffer.from(sent);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("bad signature");
}
// Safe to use the event now.
const event = JSON.parse(req.body.toString("utf8"));
console.log("Verified event:", event.event);
res.sendStatus(200);
}
);
app.listen(3000);Verify it — Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["CONNECTSAFELY_WEBHOOK_SECRET"].encode()
@app.post("/webhook")
def receive():
sent = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sent, expected):
abort(401)
event = request.get_json()
print("Verified event:", event["event"])
return "", 200Three things people get wrong
- Parsing the body before verifying. If your framework parses the JSON body first, then re-serializes it for hashing, the whitespace changes and the signature won't match. Always hash the raw bytes that arrived on the wire. (Hence
express.raw(...)above.) - Using
===to compare. A normal string compare returns as soon as it finds a different character. Attackers can measure that and brute-force the secret. Use a constant-time compare:crypto.timingSafeEqual(Node) orhmac.compare_digest(Python). - No timestamp check. An attacker who captured one real request could replay it forever. Drop requests where
X-Webhook-Timestamp(Unix seconds) is more than a few minutes old.
Rotating the secret
The signing secret is shown once and can't be retrieved later. To rotate it:
- Create a new webhook in the dashboard pointing at the same URL.
- Update your endpoint to accept either the old or new signature for a short window.
- Delete the old webhook from the dashboard.
Outbound auth (optional, defense in depth)
Signatures stop fake events from being trusted, but an attacker can still flood your URL with junk requests. If your endpoint sits behind an API Gateway, Cloudflare, or another proxy that drops unauthenticated requests at the edge, you can tell ConnectSafely to send the auth header it's expecting:
| Mode | Header ConnectSafely sends |
|---|---|
None | (no extra header) |
Bearer | Authorization: Bearer <your-token> |
API key | <HeaderName>: <HeaderValue> (you pick both) |
Basic | Authorization: Basic base64(username:password) |
The dashboard masks the credential after saving so it can never be read back.
Allow-listing by User-Agent
Every delivery from ConnectSafely identifies itself in the User-Agent header:
User-Agent: ConnectSafely-Webhooks/1.0Some teams use that for a quick allow-list at their WAF. It's a weak signal on its own (anything can set a User-Agent) — keep signature verification as your primary defense.
What to read next
What's in the payload
Beginner-friendly walkthrough of the JSON ConnectSafely sends to your webhook URL — every field explained, with an example you can copy and a tip on deduplicating retries.
Retries & Logs
What happens when your webhook endpoint is slow or down — ConnectSafely's retry policy, timeouts, delivery logs, and how to test your endpoint without waiting for a real LinkedIn event.
