Say your external client(s)/partner(s) wants to be notified of certain events happening on your system. So you decide to push events/messages to their systems via HTTP (webhooks), and your client have an end-point that accepts the requests. Here’s a security problem your client has to deal with: they have an end-point open to the public internet which could be abused by bots / bad actors. How do you let their system know that a request they have received is indeed from your service and is not a bot / spoofed / spam request?
IP allow listing is one way, but is inflexible. If you want to change your IP then you have to ask all your clients to change the allow list.
Here is a better way - HMAC: A token that you could compute from the body of the message using a secret key.
function computeToken(message: string, secret: string): string
The output is typically a Base64 SHA256 string that you can send as the “token” via a header or URL query parameter. The secret key needs to be shared with your client, so that they can compute the same token from the message body and verify if the token they computed matches the token they received in the header / URL query parameter. If both matches then the client can be sure that the request was constructed by your service and is not an invalid message.
import crypto from 'node:crypto';
function computeToken(message, secret) {
return crypto.createHmac('sha256', secretKey).update(message).digest('base64');
}
This is how Shopify’s webhook verification works for example. Note: When comparing the computed token with the token in the header / URL query parameter, be sure to use a timing safe string comparison function to protect against timing attacks (time attacks makes it easier for an attacker to figure out the token).
However there is still a potential issue with this. The token only validates that the message was constructed by your service. It doesn’t say who sent it. If an attacker gets hold of a sample body and a token, they can resend the message with the valid token over and over again.
There are ways you could help clients mitigate this. One way is to include a timestamp in the message body or header so that your clients can reject any message that is older than say 15 minutes. And/or include an ID with the message body or header so they can keep track of messages already processed in the past, and reject repeat messages.
Overall HMAC is a nice way to solve the verification problem while pushing events to external client systems. IP allow listing becomes less necessary with this approach (you could add it if you still want to).
Addition - May 11, 2023
I initially wrote this article focusing only on using HMAC on a POST body. However you could expand your imagination to add verification on a GET call as well. Lets say you have an end-point that you don’t want bots to be able to send garbage inputs. You can generate an HMAC token from the URL and add the token as a query parameter.
For example, lets say you have a URL https://example.com/avatar?display_name=John
that generates user avatar images. You could use the URL string itself to produce an HMAC (say computeToken('https://example.com/avatar?display_name=John', secret) == "be3f52a63"
) and then append that as a token to the URL to make the request verifiable (https://example.com/avatar?display_name=John&hmac=be3f52a63
). Add code to the avatar generator end-point to verify that the hmac
query parameter matches what it can compute from the rest of the URL. Next only send this URL from server side to clients via other means (for example, you could use these URLs in img
tags in an HTML page), and now only clients that use this exact URL would be allowed to see the avatars. A random person cannot use your service as a generic avatar generator with this protection in place.
That’s all for this article. Thanks for reading.