#lipefsilva_webhooks

1 messages ¡ Page 1 of 1 (latest)

boreal phoenixBOT
#

👋 Welcome to your new thread!

⏲️ We'll be here soon! Typically we respond in a few minutes, but sometimes we might take a bit longer if the server is busy or if you have a particularly tricky question.

⏱️ We close idle threads, which makes them read-only. Once a thread is closed it won't be reopened, but you can always start a new thread if you have another question.

🔗 This thread will always be available, even after it's closed. You can find it again using Discord's search, or you can save this link: https://discord.com/channels/841573134531821608/1418283792212431038

📝 Have more to share? Add more details, code, screenshots, videos, etc. below.

hot venture
#

Hi there

autumn fossil
#

Hello

#

I'll share my code snippets

hot venture
#

What error are you seeing specifically?

#

And yeah sharing code would be great

autumn fossil
#

Stripe signature validation error

#
const AWS = require('aws-sdk')
const serverless = require('serverless-http')
const express = require('express')
var bodyParser = require('body-parser')
const { Graph, GrinAuth, logger, profiler } = require('@grin-rnd/grin-api-sdk')
const { grintegrationsAuthMiddleware } = require('./middlewares')
const { initializeCloudfrontSigner } = require('../common/utils/mediaUtils')

if (process.env.IS_OFFLINE === 'true') {
  console.log(`Debug mode detected - configuring...`)
  require('dotenv').config()
  AWS.config.credentials = new AWS.Credentials({
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  })
}

const app = express()

app.use((req, res, next) => {
  if (req.originalUrl.endsWith('/v1/stripe/webhook')) {
    bodyParser.raw({ type: '*/*' })(req, res, next);
  } else {
    bodyParser.json()(req, res, next);
  }
})
#

this is the root of the API

#

This is the specific route code:

const express = require('express')
const Stripe = require('stripe')
const axios = require('axios')
const bodyParser = require('body-parser')
const { logger } = require('@grin-rnd/grin-api-sdk')

const router = express.Router()

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2019-12-03',
})

router.post(
  '/webhook',
  async (req, res) => {
    try {
      const sigHeader = req.headers['stripe-signature']

      const sig = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader.toString()
      logger.info("Stripe sig header", sig)
      // Debug information about the request body
      logger.info("Request body debug", { 
        length: req.body ? req.body.length : 0, 
        isBuffer: Buffer.isBuffer(req.body),
        bodyType: typeof req.body,
        hasBody: !!req.body,
        contentType: req.headers['content-type']
      })
      let event

      try {
        event = stripe.webhooks.constructEvent(
          req.body,
          sig,
          process.env.STRIPE_WEBHOOK_SECRET
        )
      } catch (err) {
        logger.error('⚠️ Stripe signature verification failed', { error: err.message })
        return res.status(400).send(`Webhook Error: ${err.message}`)
      }

// ...
#

and here is the error:

ERROR RequestId: 9d8893b3-0f54-44ef-9683-4c31fa48bad9 ⚠️ Stripe signature verification failed {"env":"staging","traceId":"9d8893b3-0f54-44ef-9683-4c31fa48bad9","error":"No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? \n If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.\n\nLearn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature\n"}
WARN RequestId: 9d8893b3-0f54-44ef-9683-4c31fa48bad9 400: POST /api/v1/stripe/webhook {"statusCode":400,"endpoint":"POST /api/v1/stripe/webhook","body":"Webhook Error: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? \n If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.\n\nLearn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature\n","took":2,"env":"staging","traceId":"9d8893b3-0f54-44ef-9683-4c31fa48bad9"}
hot venture
#

Yeah most likely the issue is with using bodyParser

#

You don't want to do anything to the raw request body otherwise verification will fail.

#

One thing you can do is log out the req.body before you pass it to constructEvent()

#

It should be binary here

autumn fossil
#
INFO RequestId: 36c333d6-94e1-41f0-90d3-e6030aa1f9dc Request body debug {"env":"staging","traceId":"36c333d6-94e1-41f0-90d3-e6030aa1f9dc","length":4325,"isBuffer":true,"bodyType":"object","hasBody":true,"contentType":"application/json; charset=utf-8"}
boreal phoenixBOT
autumn fossil
#

this is before constructing the event

hot venture
#

Can you log out the actual req.body?

autumn fossil
#

Sure, a sec

#

here is the body

polar vine
#

Hi there,
taking over from my colleague who had to step away.
Have you tried to implement the webhook endpoint on your integration without using bodyParser?

autumn fossil
#

Yep, but I may have missed something

#

let me try that again

#
app.use((req, res, next) => {
  if (req.originalUrl === `${process.env.API_PATH}/v1/stripe/webhook`) {
    next();
  } else {
    bodyParser.json()(req, res, next);
  }
})

@polar vine is this how it should be?

#

got the same error this way :(

polar vine
#

The log you are sharing also doesn't seem right. It should be binary.

autumn fossil
#

Let me debug this

#

I'm currently using this test route

const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2019-12-03',
});

app.post(`${process.env.API_PATH}/test-stripe-webhook`, async (req, res) => {
  const payload = req.body;
  const sig = req.headers['stripe-signature'];

  logger.info('Test route received webhook', {
    signatureHeader: sig,
    bodyType: typeof payload,
    bodyLength: payload ? payload.length : 0,
    isBuffer: Buffer.isBuffer(payload),
    contentType: req.headers['content-type']
  });

  try {
    // Try to construct the event with the webhook secret
    const event = stripe.webhooks.constructEvent(
      payload,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );

    logger.info('Signature verification successful!', {
      eventType: event.type,
      eventId: event.id
    });

    return res.status(200).json({
      success: true,
      message: 'Signature verified successfully',
      eventId: event.id,
      eventType: event.type
    });
  } catch (err) {
    logger.error('Test route signature verification failed', { error: err.message });
    return res.status(400).json({
      success: false,
      message: `Webhook Error: ${err.message}`,
      headersSent: req.headersSent
    });
  }
});
polar vine
autumn fossil
#

it is specifically that logger

#
logger.info('Test route received webhook', {
    signatureHeader: sig,
    bodyType: typeof payload,
    bodyLength: payload ? payload.length : 0,
    isBuffer: Buffer.isBuffer(payload),
    contentType: req.headers['content-type']
  });
polar vine
#

The body still looks slightly different then when I simply console.log the body of an event:
<Buffer 7b 0a 20 20 22 69 64 22 3a 20 22 65 76 74 5f 33 53 38 6c 73 67 4c 55 34 5a 71 5a 42 70 70 62 30 4c 66 6c 4f 41 61 58 22 2c 0a 20 20 22 6f 62 6a 65 63 ... 1997 more bytes>

autumn fossil
#

I'm working with serverless, do you think this could be related to the issue?

polar vine
#

Shouldn't be an issue. Let me check a few things...

polar vine
autumn fossil
#

i'm investigating to see if something else may be processing it

#

here is the full file

polar vine
#

Can you try something and add express.raw({type: "application/json"}) to your app.use funciton before calling next?

Should look like this

app.use((req, res, next) => {
  if (req.originalUrl === `${process.env.API_PATH}/v1/stripe/webhook` ||
    req.originalUrl === `${process.env.API_PATH}/test-stripe-webhook`) {
    logger.info('redirecting to stripe webhook!')
    express.raw({ type: 'application/json' })(req, res, next);
  } else {
    bodyParser.json()(req, res, next);
  }
})
#

also I would recommend removing all middleware for now to have a most basic setup

autumn fossil
#

sure

#

applied the raw arg

#

let me check if there is any middleware

#

there was only one middleware for auth, which was already being ignored

#

i removed it but got the same result

autumn fossil
# autumn fossil

this is the current error:

ERROR RequestId: 5175dc4d-8ac9-471f-be24-701f0e9b6572 Test route signature verification failed {"env":"staging","traceId":"5175dc4d-8ac9-471f-be24-701f0e9b6572","error":"No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? \n If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.\n\nLearn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature\n"}
WARN RequestId: 5175dc4d-8ac9-471f-be24-701f0e9b6572 400: POST /api/test-stripe-webhook {"statusCode":400,"endpoint":"POST /api/test-stripe-webhook","body":"{\"success\":false,\"message\":\"Webhook Error: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? \\n If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.\\n\\nLearn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature\\n\"}","took":5,"env":"staging","traceId":"5175dc4d-8ac9-471f-be24-701f0e9b6572"}

polar vine
#

Give me moment, I will talk to one of my colleagues about this.

boreal phoenixBOT
polar vine
autumn fossil
#

Yes, and no, I didn't read this part of the docs

#

let me try it

#

the first thing I tried was using the rawBody though

analog turret
#

HI 👋

I'm stepping in as my colleague needs to go.

autumn fossil
#

hey

#

can you add this other account of mine

#

it has nitro for me to send a long message

#

a sec

analog turret
#

Validating webhook signatures in Express has been a long-standing pain point. We also have this Github Issue where various strategies are discussed on how to work around it.

autumn fossil
#

@hasty basin

analog turret
#

These are limitations of the Express framework and, in your case, AWS lambda.

hasty basin
#

Applying rawBody:

const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2019-12-03',
});

app.post(`${process.env.API_PATH}/test-stripe-webhook`, async (req, res) => {
  const payload = req.rawBody;

  logger.info("raw body!", payload)
  const sig = req.headers['stripe-signature'];

  logger.info('Test route received webhook', {
    signatureHeader: sig,
    bodyType: typeof payload,
    bodyLength: payload ? payload.length : 0,
    isBuffer: Buffer.isBuffer(payload),
    contentType: req.headers['content-type']
  });

  try {
    // Try to construct the event with the webhook secret
    const event = stripe.webhooks.constructEvent(
      payload,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );

    logger.info('Signature verification successful!', {
      eventType: event.type,
      eventId: event.id
    });

    return res.status(200).json({
      success: true,
      message: 'Signature verified successfully',
      eventId: event.id,
      eventType: event.type
    });
  } catch (err) {
    logger.error('Test route signature verification failed', { error: err.message });
    return res.status(400).json({
      success: false,
      message: `Webhook Error: ${err.message}`,
      headersSent: req.headersSent
    });
  }
});

Bash:

INFO RequestId: 8cc0ba71-8a59-438a-8d7b-ab8035939db2 raw body! {"env":"staging","traceId":"8cc0ba71-8a59-438a-8d7b-ab8035939db2"}
INFO RequestId: 8cc0ba71-8a59-438a-8d7b-ab8035939db2 Test route received webhook {"env":"staging","traceId":"8cc0ba71-8a59-438a-8d7b-ab8035939db2","signatureHeader":"t=1758221502,v1=0037394d4f22a3d496625eae3b719768ef0ef3024a1adf2714276153020e4617,v0=52b8fd919fc5e6dfb45a90552d172aab097b4f2fbf1f1ba8ad2e895c55fe1ec7","bodyType":"undefined","bodyLength":0,"isBuffer":false,"contentType":"application/json; charset=utf-8"}
ERROR RequestId: 8cc0ba71-8a59-438a-8d7b-ab8035939db2 Test route signature verification failed {"env":"staging","traceId":"8cc0ba71-8a59-438a-8d7b-ab8035939db2","error":"No webhook payload was provided."}
WARN RequestId: 8cc0ba71-8a59-438a-8d7b-ab8035939db2 400: POST /api/test-stripe-webhook {"statusCode":400,"endpoint":"POST /api/test-stripe-webhook","body":"{\"success\":false,\"message\":\"Webhook Error: No webhook payload was provided.\"}","took":0,"env":"staging","traceId":"8cc0ba71-8a59-438a-8d7b-ab8035939db2"}
analog turret
#

No webhook payload was provided
THis sounds like you need to validate payload

hasty basin
#

like, the weird is that the payload is being sent by the stripe cli test trigger

#

and im not changing it in any ways

#

please take a look in the full file, with focus in the test route

analog turret
#

It does not appear to be making it to your webhook handling code

#

The error message is quite clear

hasty basin
#

it does work when I'm not validating the signature

#

if I just process it to json and not validate

analog turret
#

I would expect a much larger log line here

INFO RequestId: 8cc0ba71-8a59-438a-8d7b-ab8035939db2 raw body! {"env":"staging","traceId":"8cc0ba71-8a59-438a-8d7b-ab8035939db2"}

if you had actual data in the value

hasty basin
#

when I use the .body, instead of the .rawBody, it has a lot of data

#

let me show

#

i get this when using the .body property

analog turret
#

And that doesn't validate either , does it?

hasty basin
#

no, i get this error:

ERROR RequestId: 2807a50b-4ed0-4cd1-9d19-8729698250bb Test route signature verification failed {"env":"staging","traceId":"2807a50b-4ed0-4cd1-9d19-8729698250bb","error":"No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? \n If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.\n\nLearn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature\n"}
WARN RequestId: 2807a50b-4ed0-4cd1-9d19-8729698250bb 400: POST /api/test-stripe-webhook {"statusCode":400,"endpoint":"POST /api/test-stripe-webhook","body":"{\"success\":false,\"message\":\"Webhook Error: No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? \\n If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.\\n\\nLearn more about webhook signing and explore webhook integration examples for various frameworks at https://docs.stripe.com/webhooks/signature\\n\"}","took":2,"env":"staging","traceId":"2807a50b-4ed0-4cd1-9d19-8729698250bb"}
analog turret
#

Okay, that error can be caused by failures in the request body, signature in the headers, or the signing secret.

hasty basin
#

i was looking at this comment in the thread you sent:

Figured it out!

In the AWS admin, go to your API gateway endpoint for your webhook, then go to the integration request.
Open the Body Mapping Templates section then select application/json -- Scroll down and you'll see your body mapping template
Add this line: "rawbody": "$util.escapeJavaScript($input.body)",
I'm using Serverless, and needed to redeploy my whole stack. You'll have to do similar with whatever tool you're using to get API gateway to send the rawbody attribute (this didn't seem necessary for me for other mappings like IP address, so not sure whats going on)
In your Lambda function, you should be able to use event.rawbody as is, without unescaping. Here's my code (node.js)
var endpointSecret = "whsec_XXXXXXXX";
var headers = JSON.parse(event.headers);
var stripeEvent = stripe.webhooks.constructEvent(event.rawbody, headers["Stripe-Signature"], endpointSecret);
I was just going to filter requests by IP (checking against the list of Stripe IPs) but this is probably a more future-proof technique.

#

I don't know if that's my case yet, but may be related?

analog turret
#

I'm not certain as I don't have a lot of experience integrating with AWS lambda. We share this GH issue because it has so many crowd-sourced solutions that apply to different edge cases.

You might give it a try

hasty basin
#

Thank you

analog turret
#

You need to validate that each value is what you expect it to be.

#

It's tiresome and manual but it's really the only way.

hasty basin
#

@analog turret what should I see if I log that sig variable?

router.post('/webhook', async (req, res) => {
  try {
    let event
    if (process.env.ENV !== 'staging') {
      const sig = req.headers['stripe-signature']
      event = stripe.webhooks.constructEvent(req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET)
    } else {
      event = req.body
    }
analog turret
hasty basin
#

Yea, it seems a lot different:

{"0":"t","1":"=","2":"1","3":"7","4":"5","5":"8","6":"2","7":"2","8":"3","9":"1","10":"8","11":"9","12":",","13":"v","14":"1","15":"=","16":"1","17":"a","18":"5","19":"e","20":"b","21":"4","22":"1","23":"e","24":"8","25":"b","26":"c","27":"8","28":"7","29":"8","30":"4","31":"2","32":"7","33":"4","34":"0","35":"8","36":"b","37":"3","38":"c","39":"7","40":"3","41":"b","42":"4","43":"7","44":"b","45":"a","46":"2","47":"6","48":"1","49":"8","50":"7","51":"4","52":"a","53":"f","54":"4","55":"1","56":"1","57":"7","58":"a","59":"6","60":"7","61":"3","62":"0","63":"1","64":"e","65":"2","66":"4","67":"a","68":"5","69":"a","70":"b","71":"9","72":"c","73":"8","74":"8","75":"d","76":"7","77":"6","78":"3","79":"7","80":",","81":"v","82":"0","83":"=","84":"6","85":"6","86":"1","87":"5","88":"8","89":"9","90":"9","91":"8","92":"4","93":"3","94":"c","95":"9","96":"9","97":"6","98":"2","99":"f","100":"d","101":"c","102":"7","103":"9","104":"c","105":"5","106":"8","107":"5","108":"2","109":"b","110":"8","111":"d","112":"a","113":"d","114":"3","115":"1","116":"d","117":"5","118":"7","119":"c","120":"d","121":"7","122":"4","123":"9","124":"b","125":"3","126":"b","127":"8","128":"8","129":"9","130":"d","131":"2","132":"d","133":"6","134":"c","135":"e","136":"f","137":"7","138":"e","139":"c","140":"3","141":"f","142":"d","143":"7","144":"f","145":"8","146":"9","147":"7","env":"staging","traceId":"43c38e19-1393-46ac-bc69-b58f8ecdba7b"}
analog turret
#

Okay that looks like somehow the string got converted into an array of characters

#

It should be just a single string

hasty basin
#

@analog turret does it look correct now?

t=1758223588,v1=9112d4f617934104aa0c0abe0ca6891d55af407576a9b642e755857281ca1b08,v0=b1460803a0ed365b04f22d7d42cde236227bb483701a99583a39688c572601fb
analog turret
#

That looks better and matches the format we expect

hasty basin
#

is there a workaround from providing the rawbody?

#

i seem to not be able to receive the binary

#

maybe can I convert it into binary?

analog turret
#

Unfortunately I'm not aware of any. Usually we want to avoid any modification since even introducing an extra space would invalidate the event.

hasty basin
#

i think this modification is happening because of the API Gateway

#

let me try something

boreal phoenixBOT