Best Practices: Secure Webhook Endpoints with HMAC
When it comes to transferring data via integrations, security is a top concern. To secure data being passed via webhooks (for event-driven integrations), you have a few options, including API keys, your own authorization headers, or HMAC.
Prismatic recommends using HMAC because it's about the the simplest (and stoutest) approach you could use. It doesn't require that your team learn a new language or gain an advanced understanding of encryption, but it does an excellent job of protecting the integrity of your data at the point of transfer.
What is HMAC?
HMAC, or hashed message authentication code, is an authentication method that generates a hash from a message and a cryptographic key. When you implement HMAC for your webhook, you'll use an algorithm such as MD5, SHA-256, or RipeMD-128 for the hash to ensure the HTTP request that shows up at your webhook endpoint is legit.
How does HMAC work?
Before the source app sends an HTTP request via the webhook, it hashes the payload (request body) with HMAC using the secret key. The resulting hash is then bundled into the HTTP request as a header, and the entire request (header and body) is sent to the webhook endpoint.
Upon receiving the HTTP request, the destination app hashes the body with the secret key and then compares the result to the hash provided in the header. If the values match, the destination app knows the data is legit and processes it. If the values do not match, the destination app rejects the data and executes whatever code was written for that scenario — perhaps creating a log entry or sending a notification.
If someone tries to spoof the payload, they won't be able to generate a valid hash since they don't have the secret key. Door closed.
HMAC language support
Here are a few links to popular languages with HMAC capabilities:
Example code for HMAC
Here is an example of how HMAC might be set up in NodeJS using the built-in crypto module:
const crypto = require("crypto");
const SECRET_KEY = "secret-FA782CF7-060E-484E-B3DC-055CF2C9ED99";
const payload = JSON.stringify({
event: "REFUND_REQUEST",
user: "realcustomer@notabaddie.com",
amount: "50.25",
});
const hash = crypto
.createHmac("sha256", SECRET_KEY)
.update(payload, "utf-8")
.digest("hex");
console.log(hash); // Prints d12f95e3f98240cff00b2743160455fdf70cb8d431db2981a9af8414fc4ad5f8
The corresponding HTTP request using HMAC might look like this:
curl https://my.webhook.endpoint.com/callback \
--request POST \
--header "x-hmac-hash: d12f95e3f98240cff00b2743160455fdf70cb8d431db2981a9af8414fc4ad5f8" \
--data '{"event":"REFUND_REQUEST","user":"realcustomer@notabaddie.com","amount":"50.25"}'
Example code for HMAC multipart form data
If you're planning to use HMAC to send data as multipart form data, you'll need to hash the entire request body and use buffers to convert the data into the appropriate format. Here's an example of how you might do that in NodeJS:
const crypto = require("crypto");
const formData = require("form-data");
const fs = require("fs");
const SECRET_KEY = "secret-FA782CF7-060E-484E-B3DC-055CF2C9ED99";
// Create the Forms Data object
const data = new formData();
data.append("foo", "bar");
data.append("baz", JSON.stringify({ buz: "biz" }), {
contentType: "application/json",
});
data.append("my-buffer", Buffer.from(fs.readFileSync("/Path/To/File/")), {
filename: "name_of_file.ext",
contentType: "content/type",
});
// Create the HMAC hash for the fetch request
const hash = crypto
.createHmac("sha256", SECRET_KEY)
.update(data.getBuffer().toString())
.digest("hex");
console.log(hash);
Sending HMAC through Axios or fetch
If you're using Axios or fetch to send the HTTP request, you can include the HMAC hash in the headers like this:
With fetch, you'll need to make sure you provide the body as a buffer object and include the boundary from the form data object in the content-type header.
const response = await fetch("https://my.webhook.endpoint.com/callback", {
method: "POST",
body: data.getBuffer(),
headers: {
"x-hmac-hash": hash,
"content-type": `multipart/form-data; boundary=${data.getBoundary()}`,
},
});
console.log(response.json());
With Axios, you can include the HMAC hash in the headers like this:
axios.post("https://my.webhook.endpoint.com/callback", data, {
headers: {
"x-hmac-hash": hash,
},
});
Further resources
Setting up a HMAC for one of your integrations should be straightforward. To make things even simpler, we've added an HMAC Webhook trigger to our built in Hash component.
In many cases, this will address your needs, but for the times it doesn't, here is a quickstart tutorial for Writing a Custom Webhook Trigger with HMAC Validation.