DEV Community

Cover image for How to Secure Webhook Endpoints with HMAC
Bru Woodring for Prismatic

Posted on

How to Secure Webhook Endpoints with HMAC

Webhooks are ubiquitous in SaaS integrations, and there’s a good reason for that. They are a simple and speedy way to transfer data via HTTP callbacks between systems based on changes to data in those systems. My company helps SaaS teams build native integrations to their customers’ other apps, and many of our customers use webhooks for their integrations.

Over the last few weeks, we’ve helped several of our customers who needed to ensure that their webhook endpoints were secure. In this post, we’ll describe the approach we recommend. But first, let’s lay some groundwork.

How do webhooks work?

In short, the source app has a webhook, and the destination app has a webhook endpoint; based on some event occurring in the source app, the webhook sends an HTTP request to the webhook endpoint.

Here's a simple example of an HTTP request body (or payload):

{
  "event": "WAREHOUSE_UPDATE",
  "updates": [
    {
      "item": "gadgets",
      "action": "add",
      "quantity": 20
    },
    {
      "item": "widgets",
      "action": "remove",
      "quantity": 10
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

But how do you ensure that the destination app receives valid data from the source app and not bogus data from a bad actor who has spoofed the webhook?

The short answer is that you need to set up the webhook to provide the endpoint with the HTTP request and a unique key that the endpoint can use to verify the data. But, before we get into the details, let's briefly cover hashing.

What is hashing?

At its simplest, hashing is the process of converting a value (or key) into another value. Even if you've not worked extensively with hashing before, you are probably aware of MD5, SHA-256, or RipeMD-128. Each of these is the name of a hashing algorithm (aka cryptographic hash function).

Let's see what each algorithm does to a classic string:

  • MD5 hashes Hello World! to ed076287532e86365e841e92bfc50d8c
  • SHA-256 hashes Hello World! to 7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069
  • RipeMD-128 hashes Hello World! to 24e23e5c25bc06c8aa43b696c1e11669

The important part is that an algorithm hashes a value the same way every time. If we don't change our string ('Hello World!'), the resulting hash value doesn't change either.

However, if anything in the string changes, the hash will also change. For example, let's lower-case the 'H' so we have 'hello World!' and see what that does:

  • MD5 hashes hello World! to 41d0c351efedf7fdb9a5dc8a1ed4b4e3
  • SHA-256 hashes hello World! to e4ad0102dc2523443333d808b91a989b71c2439d7362aca6538d49f76baaa5ca
  • RipeMD-128 hashes hello World! to b5cf338f17d6796ba0312e0d78c70831

A slight change, but the resulting differences are evident.

Though hashing doesn't allow us to completely solve our original problem (someone sending bogus data to a webhook endpoint), it does lead us directly to HMAC.

What is HMAC?

HMAC, or hashed message authentication code, is an authentication method that uses not one but two keys. The first key is the HTTP request body, while the second one is a secret cryptographic key. When you implement HMAC for your webhook, you’ll be using both these keys plus an algorithm such as MD5, SHA-256, or RipeMD-128 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.

Let’s imagine that you have an e-commerce platform connected to your app. Your app regularly sends payloads to the platform’s webhook endpoint to create orders and issue refunds. Using HMAC ensures that you won’t have random (or not so random) people sending bogus orders or refunds to the e-commerce platform.

But, you say, couldn't someone capture an HTTP request and reverse engineer the hash in the header to figure out the secret? Short answer: no. Hashing is a one-way function. To crack a hash with a sufficiently complex secret, we would need more computing power and time than any of us has available.

Apps that rely on HMAC for webhook endpoints

Some well-known apps currently use HMAC to secure their webhook endpoints:

  • Slack: Provides a Signing Secret when you create a Slack app. When it sends a webhook payload, it hashes both the payload and webhook's timestamp with that secret using SHA256. The webhook request includes the resulting hash as a header called X-Slack-Signature.
  • Dropbox: Generates an App Secret when you create a Dropbox app and uses that secret to generate webhook HMAC hashes and authenticate users with OAuth 2.0. It hashes webhook payloads using SHA256 and sends the hash as a header called X-Dropbox-Signature.
  • Shopify: Creates an API Secret Key and hashes its payloads with that key and SHA256. It sends the hash as a header called X-Shopify-Hmac-SHA256.

HMAC has broad language support

You can use just about any modern language to compute HMAC hashes. Here are some links to popular languages with HMAC capabilities:

Example code for HMAC

Finally, what would all of this be without code? Here is an example of how this 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
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

Even if a bad actor intercepted your HTTP request, they couldn't issue a refund request of a million dollars to their own email address, since they couldn't sign the request properly without the secret key.

Conclusion

Using HMAC does not require that you learn a new language or gain an advanced understanding of encryption, but it does allow you to protect the integrity of the data you are transferring via webhooks.

Discussion (0)