Verifying JWT Webhooks
How to verify that a JWT webhook was sent by Moneyhub, and decode it.
When your client uses the JWT webhook format, each webhook is signed by Moneyhub. Verify every webhook before trusting its contents, so that only messages from Moneyhub are accepted by your system.
Use a well-maintained JWT library to verify and decode the token in one step. Don't decode the token without verifying it.
What to check
| Check | Expected value |
|---|---|
| Signature | Valid for the key in the Moneyhub JWKS whose kid matches the kid in the JWT header |
| Algorithm | RS256. Reject any other algorithm, including none |
iss | https://identity.moneyhub.co.uk |
aud | The client ID of your API client |
iat | Recent. We suggest rejecting tokens issued more than 10 minutes ago, with a small allowance for clock differences |
The issuer is not the OpenID configuration issuerThe
issclaim in webhooks ishttps://identity.moneyhub.co.uk. Theissuerin the OpenID configuration ishttps://identity.moneyhub.co.uk/oidc. If you copy the issuer from the OpenID configuration, verification will fail.
Read the request body as raw text and verify that exact string. If a JSON body parser or other middleware changes the body first, the signature check fails.
After verifying, map the claims to the JSON envelope fields as described in JWT claims.
Retrieve the public signing keys
Our public keys are published at the jwks_uri in our OpenID configuration, which is https://identity.moneyhub.co.uk/oidc/certs. The response looks something like:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "D9JANmYfSG_kbt2KRpTKo7QCNH2_RK-0a787yjm07zw",
"e": "AQAB",
"n": "g3oKu2O5TgkMoN_lhU8Y5-2sai9y_XPGaoZ3cf1pxdbTFdbsaBjHWjg9tBPsD8xXEEKGMNGIQBLIUVopnrtVFsWe8AI04k5PfUMWIUhto6LHOj64p2ZMRsL6N1CIYXUehmIG7esTHUcgIGmClsto6cno211ZPQjZkkqeOUzmfe0LE7oiGygXbAWtt0jNHlAw-He5tabcup1ezHZ6eAG7hklhIh4-KXICWvpWKNaqxlfX_weEWmHft18mbz1ZFGQfN0gb0VggtzKqe7QBi8SAAIPbiMre_8vEhQdfw0X_EnJnlFu-FBR984vT3eblYGU0sXkQUoD8pAUhNl3UANu0ww"
},
{
"kty": "EC",
"use": "enc",
"kid": "T7rxogsi9VUcI_SmoePSJvzif4qIUarPiFAyqTTUuYc",
"crv": "P-256",
"x": "GERoSJmHdL5VTyKTv9-XuRiNL4Y4TZnQWeTPewtW8n4",
"y": "eU5FlDQipbdr96bCSsnH9tTHckOw7gpOZ6-2uavcwnI"
}
]
}| Field | Description |
|---|---|
kty | Key type. Webhooks are signed with an RSA key |
use | How the key is used. sig keys are for signatures. Ignore enc keys for webhooks |
kid | Key ID. Match it to the kid in the JWT header to find the right key |
e | The RSA public exponent |
n | The RSA modulus |
The JWKS can contain more than one signing key, so always select the key by kid rather than taking the first sig key.
Don't hard-code the keysKeys can change over time without notice. Fetch them from the
jwks_uriand cache them. If a webhook has akidyou don't have, fetch the JWKS again. The libraries in the examples below do this for you.
Examples
These examples verify a token and return its claims. Building a Webhook Receiver shows them inside a complete receiver.
// npm install jose
import { createRemoteJWKSet, jwtVerify } from "jose"
const MONEYHUB_JWKS = createRemoteJWKSet(new URL("https://identity.moneyhub.co.uk/oidc/certs"))
export async function verifyMoneyhubWebhook(token, clientId) {
const { payload: claims } = await jwtVerify(token, MONEYHUB_JWKS, {
issuer: "https://identity.moneyhub.co.uk",
audience: clientId,
algorithms: ["RS256"],
maxTokenAge: "10m",
clockTolerance: "30s",
})
return claims
}# pip install "pyjwt[crypto]"
import time
import jwt
MONEYHUB_JWKS = jwt.PyJWKClient("https://identity.moneyhub.co.uk/oidc/certs")
def verify_moneyhub_webhook(token, client_id):
signing_key = MONEYHUB_JWKS.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=client_id,
issuer="https://identity.moneyhub.co.uk",
options={"require": ["iat", "jti", "sub", "events"]},
leeway=30,
)
if time.time() - claims["iat"] > 600:
raise jwt.InvalidTokenError("Webhook token is too old")
return claimsBoth functions raise an error if any check fails. Respond with a 4xx status and don't process the webhook.
Updated about 6 hours ago
