Verifying Webhooks
Overview
This article shows:
- What security features Remote provides for webhooks
- How webhooks can be verified
Before you get started
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
.
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
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.
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, as described in the chapter “The 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
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)
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
Note that this vulnerability is not unique to Python and can happen in any programming language where you use ==
for comparison.
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 5 months ago