❗️

Checking webhook signatures

In a production setting, it is important to verify signatures sent as part of the webhook payloads. Verifying these signatures ensures that the payloads were sent by One Codex and not a malicious third party.

In addition to the webhook payload body, we include a custom X-OneCodex-Signature HTTP header with all delivered webhooks. These signatures are generated using a hash-based message authentication code (HMAC) with SHA-256. Here's an example header:

X-OneCodex-Signature: t=1492774577c v1=d929ba98ac0e56ff425f9b8ed7c7ab631dc680f9ea80ce2f604cc75580a63b53

Where t= provides a Unix timestamp and v1= provides the v1 signature (currently the only signature scheme). The signature uses a webhook secret (defaults to the API key for your account) to sign the POST payload body and timestamp. To verify the signature of the payload, you need to:

  1. Extract the timestamp and signature from the headers
  2. Concatenate the the timestamp and request payload with a . to generate a signed payload
  3. Determine the expected signature; and finally
  4. Verify that the expected signature matches the received signature

Some brief Python 3 code for validating the signature is included for demonstration purposes below:

import hashlib
import hmac
import json

from flask import request

# Parse the request using your web framework of choice.
# Note that the entire body of the request is the payload
# and that you may need to parse the raw request body
# vs. any loaded JSON in a different language or framework
# (this code assumes a Python Flask request object).
payload = request.json
header = request.headers.get("X-OneCodex-Signature")

# 1. Extract the timestamp and signature
timestamp_part = header.split(" ")[0]
signature_part = header.split(" ")[1]
if not timestamp_part.startswith("t=") or not signature_part.startswith("v1="):
    raise Exception("Bad signature header format")

timestamp = int(timestamp_part.split("=")[1])
signature = signature_part.split("=")[1]

# 2. Generate a concatenated signed payload
signed_payload = "%d.%s" % (timestamp, payload)

# 3. Compute the SHA256 hash of your secret
secret = hashlib.sha256("YOUR_WEBHOOK_SECRET".encode("utf-8")).hexdigest().encode("utf-8") 

# 4. Determine the expected signature
expected_signature = hmac.new(
    secret, 
    signed_payload.encode(), 
    digestmod=hashlib.sha256
).hexdigest()

# 4. Verify that the expected signature matches
assert expected_signature == signature

Note: We use a similar format to Stripe for our payload signatures (they're the same except Stripe delimits the signed payload and timestamp with a comma vs. a space). See their rich documentation for additional details on why payload signatures are important and related webhook best practices.

📘

Coming soon...

In the near future, we plan to add support for parsing Event objects and verifying the signatures from a webhook payload in our onecodex Python library. This will offer an easy, one line mechanism for verifying payload POST bodies sent by our platform.