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:
1{2 "data": {3 "webhook_callback": {4 "id": "fbac1e8a-3f38-4e1a-acbe-f046761171bf",5 "signing_key": "wkyzvs764ifdrpct2naqhksmq4",6 "subscribed_events": [7 "employment.onboarding_task.completed"8 ],9 "url": "<Your URL>"10 }11 }12}
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
.
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.
4xx
or 5xx
. 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, 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:
1X-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
:
1wkyzvs764ifdrpct2naqhksmq4
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:
1{2 "company_id": "9a88cdac-4e57-46ca-afa8-580a50931cd8",3 "completed_task": {4 "action": "identity_verification",5 "completed_at": "2023-02-16T07:52:26Z",6 "description": "To help us keep you and our platform safe.",7 "name": "Verify your identity",8 "required": true,9 "status": "completed"10 },11 "employment_id": "b6e76f7c-9126-4afa-9f43-37bbc1d8e509",12 "event_type": "employment.onboarding_task.completed"13}
Then, with the headers, the raw request would look like this:
1POST / HTTP/1.12Content-Length: 3763Content-Type: application/json4X-Remote-Signature: e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef75X-Remote-Timestamp: 167781609721967{"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:
1{"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"}
Content-Length
header. If they match, then you have the right data for the signature.- The
X-Remote-Timestamp
:1677816097219
- The
X-Remote-Signature
:
1e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7
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:
1# Assigned to your webhook callback during registration2signing_key = "wkyzvs764ifdrpct2naqhksmq4"34# From the X-Remote-Timestamp header5timestamp = "1677816097219"67# From the webhook request you receive8raw_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"}'910# From the X-Remote-Signature11received_signature = "e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7"
Next, build the message that you will create the signature from:
1message = raw_request_body + ":" + timestamp23# The message looks like {"company_id":"..."}:1677816097219
:
) between the two variables!Next, you can create the HMAC object using the HMAC library provided as part of standard Python libraries:
1import hmac23hmac_object = hmac.new(signing_key.encode(), message.encode(), hashlib.sha256)
.encode()
calls on signing_key
and message
. In Python, we have to convert the Strings into Byte-arrays before passing it into hmac.new
. The programming language you use may not require you to do so.Finally, you can use this HMAC object to generate the signature:
1import base6423signature_byte_array = hmac_object.digest()4# 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'56base16_encoded_signature = base64.b16encode(signature_byte_array)7# base16_encoded_signature == b'E3F4092F158983AEA32AB25F6FECC59F64B26D45FADBED6409893F3A882ABEF7'89generated_signature = base16_encoded_signature.decode().lower()10# generated_signature == "e3f4092f158983aea32ab25f6fecc59f64b26d45fadbed6409893f3a882abef7"1112if hmac.compare_digest(received_signature, generated_signature):13 # Accept webhook request14else:15 # Reject webhook request
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.base16_encoded_signature
needs to be decoded because it's a Byte-array. Alternatively, you could convert the received_signature
to a Byte-array and then compare it to base16_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!