Securing the Endpoint
Circuit provides a way for you to be sure that the message you are receiving originated from us and not from some third-party.
This step is optional, but we highly recommend you implement it.
The 'circuit-signature' header
Circuit includes, on every request, a header called circuit-signature
. This
header should be used to assert that the message your webhook is handling is
indeed from Circuit and not from a third party.
The header contains a signature generated by using HMAC with your webhook secret as the key and the raw JSON payload of the request body as the message with the SHA256 hash function.
Obtaining your webhook secret
Secret uses a webhook secret to sign all the requests sent to your endpoint.
This secret consists of 32 characters. For example:
7fd4eb15359c04280311116c6c597041
To obtain your webhook secret do the following:
Go to your API settings page and reveal the webhook secret:
Then copy it and store it safely:
You should never share this secret. This is unique for your team and if some malicious third-party obtained it, they could forge requests to your endpoint.
If you think your secret has been compromised, Circuit has the option to regenerate it via the settings page.
Securing your endpoint with the 'signature-header'
To secure your endpoint you need to validate that the message signature you received is the same if you recalculate it from the raw payload, using your signing key.
We present here some reference implementations:
- Node
- Python
// Using express
const express = require("express");
const bodyParser = require("body-parser");
const crypto = require('crypto')
const router = express.Router();
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.text({type: '*/*'}));
router.post(‘/handle’,(request,response) => {
// validating the request
const circuitSignature = request.get('circuit-signature');
if (!receivedSignature) {
throw HttpError(400, 'Message invalid')
}
// The shared webhook secret obtained from our website
const secret = 'asdf...'
// Need to retrieve the body in the same format as it was received, don't use
// JSON.decode on an object as it may create a different string of characters
// than the one actually received. Use the unparsed body.
const message = body.raw
// Build an expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex')
// Compare the expected signature with the received one. It is recommended to
// use a timing safe compare function to prevent timing attacks.
if (
expectedSignature.length !== receivedSignature.length ||
!crypto.timingSafeEqual(
Buffer.from(expectedSignature, 0),
Buffer.from(receivedSignature, 0),
)
) {
throw HttpError(400, 'Message invalid')
}
// Now you can safely process the rest of the request here
// ...
return;
});
app.use("/", router);
# Using fast-api
import hashlib
import hmac
from typing import Dict
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
# The shared webhook secret obtained from our website
secret = b"asdf..."
@app.post("/my-api-endpoint")
async def receive_webhook(
event: Dict, request: Request, circuit_signature: str = Header(...)
):
# Validate the signature
raw_body = await request.body()
expected_signature = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
# Compare the expected signature with the received one. It is recommended to
# use a timing safe compare function to prevent timing attacks.
if not hmac.compare_digest(expected_signature, circuit_signature):
raise HTTPException(status_code=400, detail="Message invalid")
# Now you can safely process the rest of the request here
# ...
return