stripe / stripe-node

Node.js library for the Stripe API.
https://stripe.com
MIT License
3.88k stars 751 forks source link

Node: verify signature fails while using APIGatewayProxyEvent.body #1768

Closed dinilvamanan closed 1 year ago

dinilvamanan commented 1 year ago

Describe the bug

Configured webhook in stripe dashboard and trying to verify the signature for webhook requests received. We use below LoC for verification provided with essential instantiation of stripe handler and others: stripe.webhooks.constructEvent(event.body, headerSignature, webhookSecret); event: APIGatewayProxyEvent

Error we are getting:

_error.header t=1682012340,v1=58e5988431fc495e9c77dc423029fd0f6b42e1fed9066a2da6327786c2c2b58d,v0=cde35805730377bf68d61a8d96dcc9002b7117324cd4e80714e7d17623472c7a
error.message No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? Learn more about webhook signing and explore webhook integration examples for various frameworks at https://github.com/stripe/stripe-node#webhook-signing
error.payload { "id": "evt_3Myu6CGcWVbxUVwf1tPapmml", "object": "event", "apiversion": "2022-11-15", "created": 1681984120,.......}

To Reproduce

Try verifying webhook signature by using Lamda function with APIGatewayProxyEvent as the request router.

Expected behavior

Signature shall verify success

Code snippets

No response

OS

macod

Node version

v14.19.3

Library version

stripe-node

API version

2022-11-15

Additional context

No response

anniel-stripe commented 1 year ago

Hi @dinilvamanan , as the error message indicates you may not be properly passing in the raw request body you received from Stripe. https://github.com/stripe/stripe-node#webhook-signing provides more details about this. Also see related issue https://github.com/stripe/stripe-node/issues/1254, which has solutions / code samples in the follow-ups that you may find helpful.

If those resources do not help, as mentioned in https://github.com/stripe/stripe-node/issues/1254, this is more of an integration question which we typically direct to our support team at https://support.stripe.com/contact. Also feel free to join our Discord at https://stripe.com/go/developer-chat where someone can help you debug directly.

EDIT: Sorry I closed too early - looking at your example more closely, your code looks correct since APIGatewayProxyEvent.body is a string, not an object. We'll investigate this further in case this is an issue with stripe-node.

anniel-stripe commented 1 year ago

Have you confirmed that your webhook signing secret matches what is in your dashboard?

The follow-ups in https://github.com/stripe/stripe-node/issues/356 could also be helpful - you may need to add a mapping template in API Gateway to retrieve the raw body.

dinilvamanan commented 1 year ago

I confirm the secrets is directly used from dashboard, also we use stripe.wehook,generateTestHeaderString for unit testing and it all gets passed in testing. It generated consistent hash as expected only fails while verifying with request from APIGatewayProxyEvent.

anniel-stripe commented 1 year ago

Are you using an HTTP API or REST API in your AWS API Gateway? If you're using HTTP API, I believe event.body should be the raw request body out of the box, unless you have any middleware that is transforming it.

However, if you're using REST API, you may need to do some extra configuration. You can try these 2 options:

Option 1

Do you have "Use Lambda Proxy integration" turned on? If it's not turned on, request details will not be passed through to event in your handler (this will cause event.body to be undefined). In the AWS admin, go to your API gateway endpoint for your webhook, then go to Integration Request. You'll see this option:

Screenshot 2023-04-21 at 4 07 24 PM

Option 2

If you don't want to turn on "Use Lambda Proxy integration" or that does not work, you can create a mapping template. Still under Integration Request, you can create a mapping template and access event.rawBody instead:

{
  "method": "$context.httpMethod",
  "body": $input.json('$'),
  "rawBody": "$util.escapeJavaScript($input.body).replaceAll("\\'", "'")",
  "headers": {
    #foreach($param in $input.params().header.keySet())
    "$param": "$util.escapeJavaScript($input.params().header.get($param))"
    #if($foreach.hasNext),#end
    #end
  }
}

image

image

Kellokes commented 1 year ago

I have the same error here @anniel-stripe I'm using Vercel Service, Vercel use serverless functions and sounds like problem with body parser. In dev env (localhost), everything works fine.

This is my webhook code:

``` import { NextApiRequest, NextApiResponse } from "next"; import prisma from "../../lib/prismadb"; import Stripe from "stripe"; import { buffer } from "micro"; import Cors from "micro-cors"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2022-11-15", }); export const config = { api: { bodyParser: false, }, }; const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET || ""; const cors = Cors({ allowMethods: ["POST", "HEAD"], }); const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "POST") { const buf = await buffer(req); const sig = req.headers["stripe-signature"]!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( buf.toString(), sig, webhookSecret ); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; // On error, log and return the error message. if (err! instanceof Error) console.log(err); console.log(`❌ Error message: ${errorMessage}`); res.status(400).send(`Webhook Error: ${errorMessage}`); return; } // Successfully constructed event. console.log("✅ Success:", event.id); // Cast event data to Stripe object. if ( event.type === "payment_intent.succeeded" || event.type === "checkout.session.completed" ) { const paymentIntent = event.data.object as Stripe.PaymentIntent; console.log(`💰 PaymentIntent: ${JSON.stringify(paymentIntent)}`); // @ts-ignore const userEmail = paymentIntent.customer_details?.email ? paymentIntent.customer_details?.email : 'fpamarques1989@gmail.com'; let creditAmount = 0; // @ts-ignore switch (paymentIntent.amount_subtotal) { case 1000: creditAmount = 10; break; case 1800: creditAmount = 20; break; case 2500: creditAmount = 30; break; case 4990: creditAmount = 10; break; case 8990: creditAmount = 20; break; case 11990: creditAmount = 30; break; } await prisma.user.update({ where: { email: userEmail, }, data: { credits: { increment: creditAmount, }, }, }); await prisma.purchase.create({ data: { creditAmount: creditAmount, user: { connect: { email: userEmail, }, }, }, }); } else if (event.type === "payment_intent.payment_failed") { const paymentIntent = event.data.object as Stripe.PaymentIntent; console.log( `❌ Payment failed: ${paymentIntent.last_payment_error?.message}` ); } else if (event.type === "charge.succeeded") { const charge = event.data.object as Stripe.Charge; console.log(`💵 Charge id: ${charge.id}`); } else { console.warn(`🤷‍♀️ Unhandled event type: ${event.type}`); } // Return a response to acknowledge receipt of the event. Upgraded. res.json({ received: true }); } else { res.setHeader("Allow", "POST"); res.status(405).end("Method Not Allowed"); } }; export default cors(webhookHandler as any); ``` ![Captura de Tela 2023-04-24 às 10 35 56](https://user-images.githubusercontent.com/15666/234064025-ff2f6221-1303-4085-9c20-5ee6cadea20b.png)
anniel-stripe commented 1 year ago

@Kellokes since this issue deals with AWS API Gateway / Lambda functions specifically and you're using Vercel / NextJS, please open a new issue to keep the two threads separate.

Before you open a new issue though, please make sure to verify that your webhook signing secret matches what's in the dashboard, as opposed to the signing secret generated by CLI used for testing. This unfortunately is what gets a lot of developers.

Kellokes commented 1 year ago

Indeed, the key he was using was not the correct one. Everything is working correctly now. Thanks.

anniel-stripe commented 1 year ago

@dinilvamanan Were you able to resolve this issue? If so, could you share what the issue / fix was? This would be helpful for us to add better documentation for this use case.

pakrym-stripe commented 1 year ago

Closing as inactive, please feel free to reopen.