#Verge_729-webhook-verification

1 messages · Page 1 of 1 (latest)

proper frigate
#

Hello! Starting up a thread for you

#

Where did you leave off in the last thread? Have you done any more debugging since then?

#

Also you mentioned this was working for some events and not others - can you share example event IDs from a successful event and an unsuccessful one?

weary glade
#

Hello!

#

Here is the link to the last thread

native bone
#

We're happy to help if you have a concrete question!

weary glade
#

I now have made sure that I am passing the raw body to .constructEvent, but I am still getting the StripeSignatureVerificationError

native bone
#

Threads don't stay open for more than an hour, we help hundreds of developers here every week so that's not really scalable

#

But really the verification signature error is either
1/ Using the wrong secret, like you use the Stripe CLI but use the Dashboard's WebhookEndpoint's secret
2/ Not passing the raw body

weary glade
#

Thanks for the link!

native bone
#

Node.js makes this... extremely hard 😦

#

that thread is full of solutions but they are not exhaustive either

weary glade
#

Ok, so looking at those two options...
I run stripe listen on the terminal and get a webhook secret from that. This webhook secret is what I am passing into constructEvent

#

I then use the browser to trigger Stripe Events (for example, changing a subscription plan from Stripe's Customer Billing Portal)

native bone
#

yeah that's the right one to pass

weary glade
#

What part(s) of the raw body get used for verification in .constructEvent? Or is only used for creating an Event object upon successful verification?

native bone
#

I don't fully grasp your question/framing. But the whole raw body is used to construct the Event in memory. It basically deserializes the JSON into an Event class, after verifying the signature

weary glade
#

I am just trying to pinpoint what could be triggering the error because:

  1. billing_portal.session.created events pass verification, but no others do
  2. I am now passing in the raw request body
  3. I am sending the correct webhook secret

BUT... I am still getting the error StripeSignatureVerificationError with message 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

native bone
#

Unfortunately I don't have much more to offer than what I gave before

still zinc
#

Are you getting both Platform and Connect webhooks? They don't have the same secret...

weary glade
#

I was only provided with a single webhook secret

#

And that secret is a webhook signing secret coming from the Stripe CLI command stripe listen

native bone
#

do you have an Event id for when it works and doesn't work?

#

Like the evt_123 and I can check @still zinc's idea

weary glade
#

The successful has event id :evt_1LwZdvAG53kaaQs1OOLVOWou

#

And and event id for failure evt_1LwZeSAG53kaaQs13TRcjOmE

native bone
#

yeah both are on your own account and not Connect related

weary glade
#

And another failure event id evt_1LwZeSAG53kaaQs1dmh0e7g7

#

I havent seen anything on Connect up until this point. What is Connect?

#

In this case, I may need to use Connect?

still zinc
#

May well be unrelated

#

(Connect Accounts are business your platform could provide payment services for. If you're not already using it, it likely doesn't apply)

weary glade
#

Cool. Thanks @still zinc

native bone
#

Ultimately, it still has to do with the raw body, or not using UTF-8 maybe?

weary glade
#

If I pass in the raw body without adding .toString('utf8') I get the same behavior as I do by passing the raw body with the .toString('utf8') addition

#

Which is what I have mentioned: billing_portal.session.created passes but nothing else does

native bone
#

you are not supposed to ever use toString since it mutates/tampers with the data

#

what you need to do is configure your route/endpoint to handle UTF-8 upfront but it might be a red herring

still zinc
#

I can likely paste a chunk of my working code (I use Express) here if it would help (just the signing part)

#

(running in Firebase Cloud Functions)

// This example uses Express to receive webhooks
export const webhook_app = webhook_app_creator();
// The Firebase Admin SDK to access Cloud Firestore.
//const cors = require("cors");

// Automatically allow cross-origin requests
webhook_app.use(cors({ origin: true }));

// build multiple CRUD interfaces:
webhook_app.post("/direct", async (request, response) => {
  //send the response early - the only valid response is "received"
  await commonHandler(request, response, endpointDirectSecret);
  response.json({ received: true });
});

webhook_app.post("/connect", async (request, response) => {
  //send the response early - the only valid response is "received"
  await commonHandler(request, response, endpointSecret);
  response.json({ received: true });
});
//
#
const commonHandler = async (request, response, secret) => {
  const sig = request.headers["stripe-signature"];

  try {
    request.fullEvent = stripe.webhooks.constructEvent(
      request.rawBody,
      sig,
      secret
    );
  } catch (err) {
    logger(`Webhook Error: ${err.message}`);
    return;
  }

  return writeRecord("Stripe_logs", {
    Id: request.fullEvent.id,
    timestamp: serverTimestampFieldValue,
    event: request.fullEvent
  });
};
#

Catches the hook(s), verifies the signature, adds them to a processing queue, and returns status 200

#

oh, and

#
import webhook_app_creator from "express";
#

also note I explicitly DO NOT USE app.use(express.json());

#

The GitHub issue linked about covers many variations on this.

#

part of what is happening "under the hood" - Stripe uses "stegonography" to encode extra data on the webhook JSON body.  They use non-coding extra spaces, line breaks, tabs, etc.  This can still be parsed as JSON, but the signature verification needs the non-coding parts - that's why you have to be quite careful to not modify it at all before checking signature. (it also kinda masks the issue - the body parses as JSON just fine, so it looks like it's correct, but the verification fails).  This is often caused by using request.body instead of request.rawbody, or by something like Express middleware.

#

I've also noticed that, unsuprisingly, the express (req, resp) functions are often asynchronous, and processing time can affect systems that don't take that into account (i.e. my code explicitly set async (re, resp) => )

#

even in the github issue (above), a number of the folk experiencing issues were using just (req, resp) =>

weary glade
#

hmmm... my setup is a bit different... the handler function is an nhost webhook, so all the routing is done by nhost. I have been in contact with them as well on this issue, and they provided a function to create the buffer from the req since the req does not provide a raw body (see https://github.com/nhost/nhost/discussions/770)

native bone
#

ah yeah so I would explain that the raw body is crucial to verification and that it's likely a problem on their end, and making sure they receive/forward UTF-8 to you too

weary glade
#

but why does it work with billing_portal.session.created and nothing else? Does that event have different verification requirements than the others?

#

(I am also checking with nhost on this point)

native bone
#

The verification is identical

#

So that's why I can't tell you more

#

the logic is the same, it's simple code doing a basic hash signature verification on the raw body needing to match exactly what we sent you

#

any comma, space, bad characters, encoding, etc. anything changing and the signature can't match

still zinc
#

I looked at the nHook github issue, and they do not seem to understand Stripe's signature needs - they are processing the buffer they create into a string, then parsing it as JSON.

native bone
#

but then it's so strange that it works for one Event. Is it pure luck?

still zinc
#

That would be my guess. Other possibility is some form of buffer overflow.

#

this is their code, which ultimately returns JSON:

async function buffer(readable) {
  const chunks = [];
  for await (const chunk of readable) {
    chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks);
}

export default async function (req, res) {
  if (req.method === 'POST') {
    const buf = await buffer(req);
    const rawBody = buf.toString('utf8');

    res.json({ rawBody });
  } else {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
  }
}
#

it's only a sample, so not the actual handler

#

I haven't looked into the details of the buffer function (const chunk of readable), so I don't really "know" what's happening there

#

I will note the Vercel page reference has a bit more likely critical code:

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