0
votes

I have made a Stripe webhook and I want to write data to Firebase when a Stripe purchase happens, and it isn't working although the payment always succeeds but the data is not sent to Firebase database.

In Stripe console I'm getting error as:

Webhook error: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? https://github.com/stripe/stripe-node#webhook-signing

Following is the code for my webhook:

import { buffer } from "micro";
import * as admin from "firebase-admin";

// Secure a connection to firebase
const serviceAccount = require("../../../permession.json");
const app = !admin.apps.length
  ? admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
    })
  : admin.app();

// Stripe

const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

const endpointSecurit = process.env.STRIPE_SIGNING_SECRET;

const fullfillOrder = async (session) => {
  console.log("Fullfilling Order!!!");

  return app
    .firestore()
    .collection("users")
    .doc(session.metadata.email)
    .collection("orders")
    .doc(session.id)
    .set({
      amount: session.amount_total / 100,
      amount_shipping: session.total_details_amount_shipping / 100,
      images: JSON.parse(session.metadata.images),
      title: JSON.parse(session.metadata.titles),
      timestamp: admin.firestore.FieldValue.serverTimestamp(),
    })
    .then(() => {
      console.log(`SUCCESS: Order ${session.id} has been added to DB!`);
    });
};

export default async (req, res) => {
  if (req.method === "POST") {
    const requestBuffer = await buffer(req);
    const payload = requestBuffer.toString();
    const sig = req.headers["stripe-signature"];

    let event;

    // Verify (came from stripe)
    try {
      event = await stripe.webhooks.constructEvent(
        payload,
        sig,
        endpointSecurit
      );
    } catch (e) {
      console.log("ERROR", e.message);
      return res.status(400).send({ message: "Webhook error: " + e.message });
    }
    if (event.type === "checkout.session.completed") {
      const session = event.data.object;

      // Fullfill the order
      return fullfillOrder(session)
        .then(() => res.status(200))
        .catch((e) =>
          res.status(400).send({ message: "WEBHOOK_ERROR: " + e.message })
        );
    }
  }
};

export const config = {
  api: {
    bodyParser: false,
    externalResolver: true,
  },
};

Appreciate the help.

1
Can you try const payload = req.rawBody ? - Dharmaraj
@Dharmaraj I tried but it is giving the same error. - Kartik Sahu
Can you share updated code with req.rawBody? - Dharmaraj
const event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret); This should work. - skull
Getting the actual raw body in Node is notoriously difficult, as the body is prone to modifications before you can get to it. There's a long thread with many different possible solutions to this problem here if the solution proposed above doesn't work: github.com/stripe/stripe-node/issues/341 - Justin Michael

1 Answers

1
votes

If this code has been hosted on Google Cloud Functions, you must factor in the automatic body parsing done by the Functions Framework (GCF docs, Firebase docs) prior to the request being handed over to Next.js. Because the body has been consumed already, the bodyParser: false instruction is pointless for the types already handled.

To help modules that depend on it, the framework attaches a Buffer of the request's unparsed body as the property req.rawBody.

If you are using a linter or TypeScript, the req object may be typed as a express.Request object which may report that this rawBody property doesn't exist because it is no longer a part of the Express core. You will need to merge it into the type or override the type as express.Request | { rawBody: Buffer } to silence them.

Plucking the relevant lines from your code:

const requestBuffer = await buffer(req);
const payload = requestBuffer.toString();
const sig = req.headers["stripe-signature"];

const event = await stripe.webhooks.constructEvent(
  payload,
  sig,
  endpointSecurit
);

should be replaced with

const sig = req.headers["stripe-signature"];

const event = await stripe.webhooks.constructEvent(
  req.rawBody.toString(),
  sig,
  endpointSecurit
);

Make sure to also check that that your environment variables have been configured correctly.

const endpointSecurit = process.env.STRIPE_SIGNING_SECRET;
if (typeof endpointSecurit !== "string") {
  console.error("Unexpected value for STRIPE_SIGNING_SECRET");
  // potentially throw an error here
}

Side notes

If using the Firebase fork of Cloud Functions, you can also make use of functions.config() to handle your secrets instead of using environment variables.

const STRIPE_SECRET = functions.config().stripe.secret;
const STRIPE_WEBHOOK_SECRET = functions.config().stripe.webhook_secret;

Rather than silently ignore requests that aren't sent using POST, you should respond with the appropriate error:

if (req.method !== "POST") {
  res.status(405).set('Allow', 'POST').send("");
  return;
}

Don't forget to end your requests using .end(), .send(), etc.!

res.status(200).send("");

If you start to handle multiple webhook event types, consider switching to using an object of event handlers to help organise your code rather than have a large if-else/switch tree:

// before the route handler
const eventHandlers = {}; // Record<string, (event, req, res) => Promise<unknown> | unknown>

eventHandlers["checkout.session.completed"] = async (event, req, res) => {
  const session = event.data.object;
  return fullfillOrder(session)
    .then(() => res.status(200))
    .catch((e) =>
      res.status(400).send({ message: "WEBHOOK_ERROR: " + e.message })
    );
}
// in the route handler
const handler = eventHandlers[event.type];
if (!handler) {
  console.error('Ignored unexpected event type of ' + event.type);
  res.status(200).send("");
  return;
}

try {
  await Promise.resolve(handler(event, req, res))
} catch (err) {
  console.error("Unexpected error in event handler for event type of ' + event.type, err);
  res.status(500).send({ message: "Internal server error" });
}