Signing Webhooks

Available since 1.48.0

Signing Webhooks

Signing Webhook events allows you to verify each event was sent by FusionAuth. Webhook signatures provide an extra layer of security in addition to the methods described in Securing a Webhook. Signature verification provides protection against bad actors spoofing webhook event payloads to look like they came from FusionAuth.

This document covers configuring webhook signatures, verifying signatures in your webhook listener, and key rotation.

Configuration

Configuring webhook signatures in FusionAuth consists of generating a key and configuring your webhook to sign using that key.

Webhook Key Generation

Keys are generated or imported from Settings -> Key Master. Webhooks can be signed with three types of keys

  • EC key - strongest cryptography, public key can be available
  • RSA key - strong cryptography, public key can be available
  • HMAC key - fast cryptography, requires manual key distribution

EC and RSA keys allow you to make public keys available through the /.well-known/jwks.json endpoint, which facilitates key rotation. If your webhook listener cannot make outbound network connections or you prefer to manually configure your key in your webhook listener, HMAC keys are a good option.

For this example, we’ll use an RSA key pair. More information on keys is available in the Key Master Guide.

Next, you configure your webhook to sign the event with this key. From Settings -> Webhooks, click on the Edit button for your webhook (or create a new webhook). Select the Security tab panel. Once you enable Sign events , a Signing key select dropdown allows you to choose the generated key. Be sure to click Save in the upper right corner.

Webhook Settings Signature

Signature Verification

The webhook signature is provided in the HTTP header X-FusionAuth-Signature-JWT as a signed JWT with a claim of request_body_sha256 containing the SHA-256 hash of the webhook event payload.

Your webhook listener can verify the signature by:

  • Verifying the JWT is properly signed
  • Decoding the JWT
  • Comparing the JWT’s request_body_sha256 claim against your own calculated SHA-256 hash of the event body

Example webhook HTTP header

X-FusionAuth-Signature-JWT: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Il9IMDd3VkcxZlYzbDVpaDc0ck54SUMzbmV2RSJ9.eyJyZXF1ZXN0X2JvZHlfc2hhMjU2IjoiS2VWKy9IR29JUXJ4dUU1WVBDUlI2QXVRT0p2ZWxkWU5OaGJWaTFpMjJxaz0ifQ.J70gqZVuTej8FfriQqJJZecCT6XOZKH6h6Te2ir_yrSwR3luhoj_R1vAZULdrktaFPqXFXbnq9prN8j3ddelUVA5SU51J-MWVhz1bkimLo8EEdJ47ytI_97rPqVK1YJ6FSiS8_o37gablaQZv2WDbZ6ap-t4hNU5m7uwZTW9DerKg9iQjMDUIlfafEwsROLfNPfK49IsCzBNCQ8SsinVbGU0dNbs9YfMAxNzSuEKdZOIXkRNgjPfWpPnkwBbroWUrrpcoAcBSQIYFajKV-MFRISnFZ_blYps16f95iQsuTfqBkBH3r59R5tFBP66FA1bvQJZVlAHJfdNTXnXx2F2BQ

The JWT decodes with:

JWT header

{
   "alg": "RS256",
   "typ": "JWT",
   "kid": "_H07wVG1fV3l5ih74rNxIC3nevE"
 }

JWT payload

{
  "request_body_sha256": "KeV+/HGoIQrxuE5YPCRR6AuQOJveldYNNhbVi1i22qk="
}

The kid identifies the Id of the key used to sign the JWT. JWT libraries can look the key up from the JWKS endpoint, or a locally stored key can be used. After verifying the JWT signature, the JWT’s request_body_sha256 payload claim is compared against your own calculated SHA-256 hash of the event body

Example Webhook Listener Code

The following code (available on GitHub) demonstrates webhook signature verification with a simple Node server.

Example Node.js Webhook Signature Verifier

const bodyParser = require('body-parser');
const express = require("express");
const crypto = require("crypto");
const jose = require("jose");

// configure these
const port = 3030;
const webhookListenerPath = '/webhook';
const fusionauthJwksEndpoint = 'https://local.fusionauth.io/.well-known/jwks.json'

const signatureHeader = 'X-FusionAuth-Signature-JWT'

const app = express();
app.use(bodyParser.json({
  type:'*/*',
  limit: '50mb',
  verify: function(req, res, buf) {
    req.rawBody = buf;
  }
 })
);

const cachedRemoteJWKS = jose.createRemoteJWKSet(new URL(fusionauthJwksEndpoint))

app.post(webhookListenerPath, async function (req, res) {
  console.log("\n req.headers: " + JSON.stringify(req.headers));

  const hashPayload = req.rawBody;
  console.log("\n req.rawBody: " + hashPayload);

  const jwt = Buffer.from(req.get(signatureHeader) || '', 'utf8');

  try {
    const { payload, protectedHeader } = await jose.jwtVerify(jwt, cachedRemoteJWKS);

    const body_sha256 = crypto.createHash('sha256').update(hashPayload).digest('base64');

    // Compare digest signature with signature sent by provider
    if (payload.request_body_sha256 === body_sha256) {
      console.log("Valid signature");
      // Do your webhook event processing here
      res.json({ message: "Success" });
    } else {
      console.log("Invalid signature");
      // skip this event
      res.status(401).send('Unauthorized');
    }
  } catch (err) {
    console.log("Invalid JWT header");
    res.status(401).send('Unauthorized');
  }
});

app.listen(port, function () {
  console.log(`Example app listening on port ${port}!`);
});

Testing

The Webhook Testing page provides a quick way to test your webhook signature configuration and signature verification on your webhook listener.

Key Rotation

Rotating keys regularly is an important part of a defense-in-depth strategy. The type of key used for signing webhook events and the method used for fetching that key determines the process for rotating keys.

  • Signatures validated using a public key (RSA or EC) where signature verification dynamically fetches public key from .well-known/jwks.json endpoint
    • Generate new key in FusionAuth
    • Update webhook signing key to use new key
    • Test
    • Delete old key
  • Other cases
    • Generate new key
    • Update your webhook listener to accept new key in addition to old key
    • Update webhook to use new key
    • Test
    • Update your webhook listener to only accept new key
    • Delete old key from FusionAuth