svix / svix-webhooks

The enterprise-ready webhooks service 🦀

Home Page:https://www.svix.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Issue with verifying a webhook using Svix in my Express.js app

MichaelvdVeer opened this issue · comments

Bug Report

Hi everyone, I’m having an issue with verifying a webhook using Svix in my Express.js app.

Version

svix@1.35.0

Platform

Clerk Authentication and User Management
Node version v20.17.0
Express version 4.21.0

Description

According to the Svix documentation, I should pass the raw payload directly into the Webhook.verify() method, but that’s not working for me.

I tried this code:

import express from "express";
import { Webhook } from "svix";
import bodyParser from "body-parser";
const router = express.Router();
router.post(
  "/clerk",
  bodyParser.raw({ type: "application/json" }), // Using raw body-parser
  async (req, res) => {
    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
    const payload = req.body;
    const headers = req.headers;
    const svix_id = headers["svix-id"];
    const svix_timestamp = headers["svix-timestamp"];
    const svix_signature = headers["svix-signature"];
    const wh = new Webhook(WEBHOOK_SECRET);
    let evt;
    try {
      evt = wh.verify(payload, {  
        "svix-id": svix_id,
        "svix-timestamp": svix_timestamp,
        "svix-signature": svix_signature,
      });
    } catch (err) {
      console.log("Error verifying the webhook:", err.message);
      return res.status(400).json({ success: false, message: "Verification failed." });
    }
    // Further processing of the webhook event...
    return res.status(200).json({ success: true });
  }
);
export default router;

I expected to see this happen: My database syncs with Clerk user data.

Instead, this happened: I get this error: Error verifying the webhook: Expected payload to be of type string or Buffer. Please refer to https://docs.svix.com/receiving/verifying-payloads/how for more information.

Oddly enough, when I JSON.stringify() the payload before passing it to verify(), everything works fine:

  try {
      evt = wh.verify(JSON.stringify(payload), {
        "svix-id": svix_id,
        "svix-timestamp": svix_timestamp,
        "svix-signature": svix_signature,
      });
    } catch (err) {
      console.log("Error verifying the webhook:", err.message);
      return res.status(400).json({
        success: false,
        message: "Webhook verification failed.",
      });
    }

I am having the same issue:
Error verifying webhook: Expected payload to be of type string or Buffer. Please refer to https://docs.svix.com/receiving/verifying-payloads/how for more information.
when I log the event it returns undefined.
Could this be a middleware issue?

I have learned that it sounds like Express already deserializes the JSON body into a JavaScript object. It works with JSON.stringify() because there were no (whitespace, field ordering, etc.) differences between the original body and what JSON.stringify() produced. However, you should not rely on that always being the case, which is why they have a warning in the documentation about using JSON.stringify().

Indeed. You need to use the body parser to prevent that behavior: https://docs.svix.com/receiving/verifying-payloads/how#nodejs-express

Hi @tasn , thanks for the reply. I'm using body-parser, but I still get this error message:

Error verifying the webhook: Expected payload to be of type string or Buffer. Please refer to https://docs.svix.com/receiving/verifying-payloads/how for more information.

import express from "express";
import { Webhook } from "svix";
import bodyParser from "body-parser";
import createUserAccountWithProfile from "../services/webhooks/createUserAccountWithProfile.js";
import updateUserAccountWithProfile from "../services/webhooks/updateUserAccountWithProfile.js";

const router = express.Router();

router.post(
  "/clerk",
  bodyParser.raw({ type: "application/json" }),
  async (req, res, next) => {
    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
    if (!WEBHOOK_SECRET) {
      return next(
        new Error("You must have a WEBHOOK_SECRET in your .env file.")
      );
    }

    const payload = req.body;
    const headers = req.headers;

    const wh = new Webhook(WEBHOOK_SECRET);

    let evt;

    try {
      evt = wh.verify(payload, headers);
    } catch (err) {
      console.log("Error verifying the webhook:", err.message);
      return res.status(400).json({
        success: false,
        message: "Webhook verification failed.",
      });
    }

    const eventType = evt.type;

    if (eventType === "user.created") {
      const { id, email_addresses, first_name, last_name, image_url } =
        evt.data;

      let fullName = [first_name, last_name].filter(Boolean).join(" ").trim();

      if (!first_name && !last_name) {
        fullName = null;
      }

      const email = email_addresses[0].email_address;
      const imageUrl = image_url;
      const acceptTermsPrivacyAndAdult = true;

      try {
        await createUserAccountWithProfile(
          id,
          email,
          fullName,
          imageUrl,
          acceptTermsPrivacyAndAdult
        );

        console.log(
          `User with ID ${id} has been created and saved to the database.`
        );

        return res.status(200).json({
          success: true,
          message: "User created and saved to the database.",
        });
      } catch (error) {
        console.error("Error saving the user:", error);
        return res.status(500).json({
          success: false,
          message: "Internal server error while saving the user.",
        });
      }
    } else if (eventType === "user.updated") {
      const {
        id,
        email_addresses,
        username,
        first_name,
        last_name,
        image_url,
      } = evt.data;

      let fullName = [first_name, last_name].filter(Boolean).join(" ").trim();

      if (!first_name && !last_name) {
        fullName = null;
      }

      const email = email_addresses[0].email_address;
      const imageUrl = image_url;
      const activated = true;

      try {
        await updateUserAccountWithProfile(
          id,
          email,
          username,
          fullName,
          imageUrl,
          activated
        );

        console.log(`User with ID ${id} has been updated in the database.`);

        return res.status(200).json({
          success: true,
          message: "User updated in the database.",
        });
      } catch (error) {
        console.error("Error updating the user:", error);
        return res.status(500).json({
          success: false,
          message: "Internal server error while updating the user.",
        });
      }
    } else {
      console.log(`Unknown event type received: ${eventType}`);
      return res.status(400).json({
        success: false,
        message: `Unknown event type: ${eventType}`,
      });
    }
  }
);

export default router;

You have to wrap the payload -- where you first instantiate it -- in a JSON.stringify(req.body)

@celestelayne, you shouldn't need to do that (and it'll probably break things down the line, even if make it work now).

@MichaelvdVeer, can you try asking the Express people? Seems to be an issue there?

@tasn But is it true that, according to the Svix docs, unlike in Express, you do need to use JSON.stringify in Fastify?

I will ask Express if they have an answer about this issue.

@celestelayne
@tasn

With help from Express, I found the solution. In my index.js file, I had app.use(express.json()) declared before the webhook route, which was causing the body to be parsed as JSON before it reached the middleware that expects a raw Buffer.

I’ve now moved app.use(express.json()) to be placed after the webhook route, and that resolved the issue.

Here is my current code. Can you confirm if this is the right way?

import express from "express";
import { Webhook } from "svix";
import bodyParser from "body-parser";
import createUserAccountWithProfile from "../services/webhooks/createUserAccountWithProfile.js";
import updateUserAccountWithProfile from "../services/webhooks/updateUserAccountWithProfile.js";

const router = express.Router();

router.post(
  "/clerk",
  bodyParser.raw({ type: "application/json" }),
  async (req, res, next) => {
    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
    if (!WEBHOOK_SECRET) {
      return next(
        new Error("You must have a WEBHOOK_SECRET in your .env file.")
      );
    }

    const payload = req.body;
    const headers = req.headers;

    const svix_id = headers["svix-id"];
    const svix_timestamp = headers["svix-timestamp"];
    const svix_signature = headers["svix-signature"];

    if (!svix_id || !svix_timestamp || !svix_signature) {
      return res.status(400).json({
        success: false,
        message: "An error occurred -- no svix headers present.",
      });
    }

    const wh = new Webhook(WEBHOOK_SECRET);

    let evt;

    try {
      evt = wh.verify(payload, {
        "svix-id": svix_id,
        "svix-timestamp": svix_timestamp,
        "svix-signature": svix_signature,
      });
    } catch (err) {
      console.log("Error verifying the webhook:", err.message);
      return res.status(400).json({
        success: false,
        message: "Webhook verification failed.",
      });
    }

    const eventType = evt.type;

    if (eventType === "user.created") {
      const { id, email_addresses, first_name, last_name, image_url } =
        evt.data;

      let fullName = [first_name, last_name].filter(Boolean).join(" ").trim();

      if (!first_name && !last_name) {
        fullName = null;
      }

      const email = email_addresses[0].email_address;
      const imageUrl = image_url;
      const acceptTermsPrivacyAndAdult = true;

      try {
        await createUserAccountWithProfile(
          id,
          email,
          fullName,
          imageUrl,
          acceptTermsPrivacyAndAdult
        );

        console.log(
          `User with ID ${id} has been created and saved to the database.`
        );

        return res.status(200).json({
          success: true,
          message: "User created and saved to the database.",
        });
      } catch (error) {
        console.error("Error saving the user:", error);
        return res.status(500).json({
          success: false,
          message: "Internal server error while saving the user.",
        });
      }
    } else if (eventType === "user.updated") {
      const {
        id,
        email_addresses,
        username,
        first_name,
        last_name,
        image_url,
      } = evt.data;

      let fullName = [first_name, last_name].filter(Boolean).join(" ").trim();

      if (!first_name && !last_name) {
        fullName = null;
      }

      const email = email_addresses[0].email_address;
      const imageUrl = image_url;
      const activated = true;

      try {
        await updateUserAccountWithProfile(
          id,
          email,
          username,
          fullName,
          imageUrl,
          activated
        );

        console.log(`User with ID ${id} has been updated in the database.`);

        return res.status(200).json({
          success: true,
          message: "User updated in the database.",
        });
      } catch (error) {
        console.error("Error updating the user:", error);
        return res.status(500).json({
          success: false,
          message: "Internal server error while updating the user.",
        });
      }
    } else {
      console.log(`Unknown event type received: ${eventType}`);
      return res.status(400).json({
        success: false,
        message: `Unknown event type: ${eventType}`,
      });
    }
  }
);

export default router;