Webhook Signature Verification
Verify the authenticity of webhook calls
All webhook calls include a JWT signature that you can use to verify the authenticity and integrity of the event.
Signature Header
Lirium's webhook POST requests include an HTTP header:
X-JWT-SIGNATURE: <jwt-token>
The JWT is signed using the RS512 algorithm.
JWT Structure
{
"iss": "[SIGNING_KEY_ID]",
"iat": 1646758802,
"digest": "[REQUEST_BODY_DIGEST]"
}| Claim | Description |
|---|---|
iss | Signing key ID: lirium-sandbox or lirium-production |
iat | Unix timestamp when the JWT was generated |
digest | Hex-encoded SHA-256 hash of the request body |
Verification Steps
- Extract the JWT from the
X-JWT-SIGNATUREheader - Decode and verify the JWT using Lirium's public key
- Compute the SHA-256 hash of the request body
- Compare the computed hash with the
digestclaim - Optionally validate the
iattimestamp is recent
Python Example
from jose import jwt
from hashlib import sha256
import flask
# Get the JWT from the request header
token = flask.request.headers.get("X-JWT-SIGNATURE")
# Verify and decode the JWT
decoded = jwt.decode(token, signing_public_key, algorithms=["RS512"])
# Compute the digest of the request body
digest = sha256(flask.request.data).hexdigest()
# Verify the digest matches
assert decoded.get("digest") == digest
# Verify the issuer matches your environment
assert decoded.get("iss") == "lirium-sandbox" # or "lirium-production"Public Keys
Get the public keys for signature verification from Webhooks Public Keys.
Example JWT
Decoded payload:
{
"iss": "lirium-sandbox",
"iat": 1646758802,
"digest": "075b83598e4e147f240f5b15694b42063741b677548c89c9a4f100b5a36263f5"
}Encoded JWT:
eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJsaXJpdW0tc2FuZGJveCIsImlhdCI6MTY0Njc1ODgwMiwiZGlnZXN0IjoiMDc1YjgzNTk4ZTRlMTQ3ZjI0MGY1YjE1Njk0YjQyMDYzNzQxYjY3NzU0OGM4OWM5YTRmMTAwYjVhMzYyNjNmNSJ9.ONcsg4ZWPKVuXC_nGWk7zfKuoqWgfV4edxDeweL7S4cSkj9SdnxZnxSBdPFMzeIaPAETnFNc-6Pxcd919pYPIx0M155yEBP4SHwmsflh93vrjdoUZYB2sD3XL6xeiCQ0KuwRp_yHpICO9JzCto4XsbvZ_wmhI1wIl-hDnlPDfdbL--rV-usbx1EPXoS67v3oTV2YtdvtoO5QZAy7relxyA9hLEpsKv_Duf_RYDiNkGEl_aZrKu8fQEFf0CUZkbqsbv1Xt-VBavk1A3muGXJfOHjBxbT5er0_Mkn7pj5T1o4XiRLlz-WhYjwsKaewpvsdoFVt_ggFPOwcBYaOLKPx5Xfapt7WtsxSH3gds9PzUGj9mi4lIm-wCYjIn7tb9iimwfhkNm_6himIflLGCQ1eu3n1UzirnX5OvlmBOutXkHddWxzp1xBn5PuWWRMMHGWg-tZcUl5x4cV9kkkse9FmheHkstjf0Ham3ePCydlMp5r0vjrgsXbHaC3iLSf_3al7wpkLXogWiVVbqAkoLYpy3nCTZ_i0btnn5GwcRW2Gx5-ILNoxLe49lMD_le-iSEb9VaC7n8lyZYw3WTXdN1a0RAAO3RbkT8UahqTxfH_iqWbbRcr1Uep92OKTt3xbaCqvyGMOSLSO-nlnXrLgyZHllG0K59wUwQjp2GcMVOljoNUUpdated 4 months ago
