Verifying Webhooks
Overview
This article shows:
- What security features Remote provides for webhooks
- How webhooks can be verified
Prerequisites
Before you get started, you should be familiar with registering webhook callbacks with the Remote API .
Webhook Security Features
When you have an endpoint in your system that can accept requests from the outside world, it’s possible that bad actors will attempt to exploit this endpoint. This is a problem especially when such an endpoint accepts and modifies data in your system.
To overcome this problem, you need a way to verify requests that are made to this endpoint, such that you can differentiate bad actors from expected requests. The Remote API allows you to verify the authenticity of webhook requests by using cryptographically-signed signatures.
The Signing Key
When you register a webhook callback URL with the POST /v1/webhook-callbacks
endpoint, the response will always include a signing_key
. Here’s an example:
{
"data": {
"webhook_callback": {
"id": "fbac1e8a-3f38-4e1a-acbe-f046761171bf",
"signing_key": "wkyzvs764ifdrpct2naqhksmq4",
"subscribed_events": [
"employment.onboarding_task.completed"
],
"url": "<Your URL>"
}
}
}
The signing_key
returned from this endpoint is unique to this webhook callback. All requests made to the URL you registered (with the POST /v1/webhook-callbacks
endpoint) will include a signature signed with the signing_key
returned in the response for a call to POST /v1/webhook-callbacks
.
⚠️ The signing key is a secret and should not be exposed outside your systems. Anyone with access to your signing key can send webhook requests to your webhook URL and make it appear legitimate.
If you suspect that the signing key has been leaked, make sure to delete the webhook callback and register the same URL again to obtain a different signing key.
You will need this signing_key
to re-create the signature on your end.
Webhook Signature and Timestamp
When you receive a webhook request from Remote, it will include 2 custom HTTP headers: X-Remote-Timestamp
and X-Remote-Signature
.
The Webhook Timestamp
The webhook timestamp is a Unix timestamp of millisecond-precision. Remote uses it to generate part of the signature sent in X-Remote-Signature
. This timestamp represents the first time Remote makes an attempt to send the webhook request. Once you verify the signature in X-Remote-Signature
, you can use this timestamp to ignore old requests.
ℹ️ Remote retries sending webhook requests if the recipient (i.e. the URL you provided for when registering the webhook callback) responds with
4xx
or5xx
. In such cases, an old, retried webhook request will have an old timestamp, while a new webhook request for the same type of event will have a newer timestamp.Having access to the timestamp in this case will allow you to ignore an old, retried webhook request.
The Webhook Signature
The webhook signature is a cryptographically-signed hash that is unique to every webhook request. Remote uses the following to generate the signature:
- A signing key
- The unformatted, raw webhook request body
- The timestamp included sent in the
X-Remote-Timestamp
header
Remote uses the Hash-based Message Authentication Code (HMAC) cryptographic technique to create the signatures, with SHA256 as the hash function. The signature included in the X-Remote-Signature
header is Base 16 encoded.
Here’s an example of the X-Remote-Signature
header:
X-Remote-Signature: e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7
Using the signature to verify authenticity
The HMAC cryptographic technique generates a one-way hash, so you cannot decrypt it to get its contents. Instead, you will need to generate the signature on your end and ensure that the signature you generate matches what Remote sent in the X-Remote-Signature
header.
Collecting the required data to generate the signature
For this example, assume that you will be using the response sent in the example above, demonstrating the response from POST /v1/webhook-callbacks
. From this response, you will need the signing_key
:
wkyzvs764ifdrpct2naqhksmq4
Next, you will need the unformatted, raw request body from the webhook request you receive. For example, if you received a webhook request with the following body:
{
"company_id": "9a88cdac-4e57-46ca-afa8-580a50931cd8",
"completed_task": {
"action": "identity_verification",
"completed_at": "2023-02-16T07:52:26Z",
"description": "To help us keep you and our platform safe.",
"name": "Verify your identity",
"required": true,
"status": "completed"
},
"employment_id": "b6e76f7c-9126-4afa-9f43-37bbc1d8e509",
"event_type": "employment.onboarding_task.completed"
}
Then, with the headers, the raw request would look like this:
POST / HTTP/1.1
Content-Length: 376
Content-Type: application/json
X-Remote-Signature: e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7
X-Remote-Timestamp: 1677816097219
{"company_id":"9e88cdac-4e57-46ca-a5a8-580150935cd8","completed_task":{"action":"identity_verification","completed_at":"2023-02-16T07:52:26Z","description":"To help us keep you and our platform safe.","name":"Verify your identity","required":true,"status":"completed"},"employment_id":"b6e66f7c-9026-4afc-9f43-37bb31a8e509","event_type":"employment.onboarding_task.completed"}
From this webhook request, you would need the following:
- The raw request body:
{"company_id":"9e88cdac-4e57-46ca-a5a8-580150935cd8","completed_task":{"action":"identity_verification","completed_at":"2023-02-16T07:52:26Z","description":"To help us keep you and our platform safe.","name":"Verify your identity","required":true,"status":"completed"},"employment_id":"b6e66f7c-9026-4afc-9f43-37bb31a8e509","event_type":"employment.onboarding_task.completed"}
ℹ️ It's important that the raw request is unformatted. Otherwise, you won't get the same signature as the one Remote sends you.
One way to ensure you’re using the raw request body is to compare its length to the
Content-Length
header. If they match, then you have the right data for the signature. - The
X-Remote-Timestamp
:1677816097219
- The
X-Remote-Signature
:e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7
Now that you have all the required data, you can generate the signature.
Generating and comparing the signature
You can generate the signature and compare it to the one you receive in the X-Remote-Signature
header in any programming language that has HMAC libraries. This guide will show you how to do it in Python, using the standard Python libraries.
First, assign all the data you need to variables:
# Assigned to your webhook callback during registration
signing_key = "wkyzvs764ifdrpct2naqhksmq4"
# From the X-Remote-Timestamp header
timestamp = "1677816097219"
# From the webhook request you receive
raw_request_body = '{"company_id":"9e88cdac-4e57-46ca-a5a8-580150935cd8","completed_task":{"action":"identity_verification","completed_at":"2023-02-16T07:52:26Z","description":"To help us keep you and our platform safe.","name":"Verify your identity","required":true,"status":"completed"},"employment_id":"b6e66f7c-9026-4afc-9f43-37bb31a8e509","event_type":"employment.onboarding_task.completed"}'
# From the X-Remote-Signature
received_signature = "e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7"
Next, build the message that you will create the signature from:
message = raw_request_body + ":" + timestamp
# The message looks like {"company_id":"..."}:1677816097219
ℹ️ The format of the message should be as shown here, regardless of the programming language you use. Don’t forget to add the colon (
:
) between the two variables.
Next, you can create the HMAC object using the HMAC library provided as part of standard Python libraries:
import hmac
hmac_object = hmac.new(signing_key.encode(), message.encode(), hashlib.sha256)
ℹ️ Notice the
.encode()
calls onsigning_key
andmessage
. In Python, we have to convert the Strings into Byte-arrays before passing it intohmac.new
. The programming language you use may not require you to do so.
Finally, you can use this HMAC object to generate the signature:
import base64
signature_byte_array = hmac_object.digest()
# signature_byte_array == b'\xe3\xf4\t/\x15\x89\x83\xae\xa3*\xb2_o\xec\xc5\x9fd\xb2mE\xfa\xdb\xedd\t\x89?:\x88*\xbe\xf7'
base16_encoded_signature = base64.b16encode(signature_byte_array)
# base16_encoded_signature == b'E3F4092F158983AEA32AB25F6FECC59F64B26D45FADBED6409893F3A882ABEF7'
generated_signature = base16_encoded_signature.decode().lower()
# generated_signature == "e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7"
if hmac.compare_digest(received_signature, generated_signature):
# Accept webhook request
else:
# Reject webhook request
⚠️ Python recommends using the
compare_digest()
function instead of the==
operator to reduce the vulnerability to timing attacks. You can read more about this kind of timing attack here .Note that this vulnerability is not unique to Python and can happen in any programming language where you use
==
for comparison.
ℹ️ The
base16_encoded_signature
needs to be decoded because it's a Byte-array. Alternatively, you could convert thereceived_signature
to a Byte-array and then compare it tobase16_encoded_signature
instead.Regardless of your comparison method, remember that
X-Remote-Signature
is Base-16 encoded.
Since the received_signature
matches generated_signature
in this case, you have successfully authenticated the webhook request as having originated from Remote!
Updated 2 days ago