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
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(" ") signature_part = header.split(" ") if not timestamp_part.startswith("t=") or not signature_part.startswith("v1="): raise Exception("Bad signature header format") timestamp = int(timestamp_part.split("=")) signature = signature_part.split("=") # 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.
In the near future, we plan to add support for parsing
Eventobjects 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.