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:
- Extract the timestamp and signature from the headers
- Concatenate the the timestamp and request payload with a
.
to generate a signed payload - Determine the expected signature; and finally
- 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.