Search code examples
javascriptnode.jsexpressstripe-payments

Resolving Webhook Signature Verification Issues in Stripe with Express.js


I've been trying to implement Stripe webhooks in my Express.js application, but I'm facing issues with webhook signature verification. The error message I'm receiving is:

⚠️  Webhook signature verification failed. Webhook payload must be provided as a string or a Buffer (https://nodejs.org/api/buffer.html) instance representing the _raw_ request body.Payload was provided as a parsed JavaScript object instead. Signature verification is impossible without access to the original signed material.

Here's my current setup:

index.js

require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = 8080;
const cors = require("cors");
const mongoose = require("mongoose");

app.use(cors());

app.get("/", (req, res) => {
  res.send("Hello, World!");
});

// Use these parsers for other routes
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));

const dbURI = process.env.MONGODB_CONNECTION;

const searchRouter = require("./routes/searchRoutes");
const phoneRouter = require("./routes/phone");
const paymentRouter = require("./routes/payment");
const linkRouter = require("./routes/link");
const pieceRouter = require("./routes/piece");
const userRouter = require("./routes/user");
const emailRouter = require("./routes/email");
const autocompleteRouter = require("./routes/autocomplete");
const webhookRouter = require("./routes/webhook");

app.use("/", searchRouter);
app.use("/phone", phoneRouter);
app.use("/payment", paymentRouter);
app.use("/link", linkRouter);
app.use("/piece", pieceRouter);
app.use("/user", userRouter);
app.use("/email", emailRouter);
app.use("/autocomplete", autocompleteRouter);
app.use("/webhook", webhookRouter);

mongoose
  .connect(dbURI)
  .then(() => {
    console.log("MongoDB connected");
    app.listen(port, () => {
      console.log(`Server running on http://localhost:${port}/`);
    });
  })
  .catch((err) => console.log(err));

webhook.js

require("dotenv").config();
const express = require("express");
const router = express.Router();
const stripe = require("stripe")(process.env.STRIPE_SECRET_TEST_KEY);

const endpointSecret = process.env.STRIPE_TEST_WEBHOOK_KEY;

router.post("/", async (req, res) => {
  const sig = req.headers["stripe-signature"];

  console.log("Webhook received!");

  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log(`⚠️  Webhook signature verification failed.`, err.message);
    return res.sendStatus(400);
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object;

    try {
      const paymentIntent = await stripe.paymentIntents.retrieve(
        session.payment_intent,
        {
          expand: ["charges"],
        }
      );

      const charge = paymentIntent.charges.data[0];
      const transferId = charge.transfer;
      const taxAmount = session.total_details.amount_tax;

      await stripe.transfers.createReversal(transferId, {
        amount: taxAmount,
      });

      console.log(`Successfully reversed tax amount: ${taxAmount}`);
    } catch (error) {
      console.error("Error creating transfer reversal:", error);
    }
  }

  res.sendStatus(200);
});

module.exports = router;

What am I missing here? How can I properly handle the raw request body for Stripe webhook signature verification?

I've tried a number of middleware and looked at a few articles so far but none of them seem to work for me. I don't know if this matters but I have been using ngrok to expose the local endpoint to test on.

Already checked these articles: Stripe - Webhook payload must be provided as a string or a Buffer https://github.com/stripe/stripe-node/issues/341 https://github.com/stripe/stripe-node#webhook-signing Stripe Webhook 400 error. Raw request body issues


Solution

  • You have bodyParser and that's problematic with Express and Stripe Webhook Signature. Take a look at this issue and try the suggested workaround (assuming you rename your endpoint to /stripe-webhooks)

    app.use(bodyParser.json({
    // Because Stripe needs the raw body, we compute it but only when hitting the Stripe callback URL.
    verify: function(req,res,buf) {
        var url = req.originalUrl;
        if (url.startsWith('/stripe-webhooks')) {
            req.rawBody = buf.toString()
        }
    }}));
    

    or avoid bodyParser and strictly following Stripe provided sample code.