#Verge_729-webhook-verification
1 messages · Page 1 of 1 (latest)
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?
We're happy to help if you have a concrete question!
I now have made sure that I am passing the raw body to .constructEvent, but I am still getting the StripeSignatureVerificationError
Threads don't stay open for more than an hour, we help hundreds of developers here every week so that's not really scalable
https://github.com/stripe/stripe-node/issues/341 we recommend reading this closely, it has numerous potential solutions
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
Thanks for the link!
Node.js makes this... extremely hard 😦
that thread is full of solutions but they are not exhaustive either
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)
yeah that's the right one to pass
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?
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
I am just trying to pinpoint what could be triggering the error because:
billing_portal.session.createdevents pass verification, but no others do- I am now passing in the raw request body
- 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
Unfortunately I don't have much more to offer than what I gave before
Are you getting both Platform and Connect webhooks? They don't have the same secret...
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
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
The successful has event id :evt_1LwZdvAG53kaaQs1OOLVOWou
And and event id for failure evt_1LwZeSAG53kaaQs13TRcjOmE
yeah both are on your own account and not Connect related
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?
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)
Cool. Thanks @still zinc
Ultimately, it still has to do with the raw body, or not using UTF-8 maybe?
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
yeah unfortunately it could be anything in your code and I don't have anything else I know of that can help narrow it down except carefully reading https://github.com/stripe/stripe-node/issues/341
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
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) =>
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)
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
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)
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
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.
but then it's so strange that it works for one Event. Is it pure luck?
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,
},
};