Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vercel Production API bodyParser (stripe webhooks) #1410

Closed
BurnedChris opened this issue Oct 28, 2020 · 14 comments
Closed

Vercel Production API bodyParser (stripe webhooks) #1410

BurnedChris opened this issue Oct 28, 2020 · 14 comments

Comments

@BurnedChris
Copy link
Contributor

BurnedChris commented Oct 28, 2020

Hello,

When i have been building my application everything worked fine and no problems with stripe passing through webhooks to redwood API. Once Built to Vercel, the webhooks no longer work, and it's critical they do work!

  if (event.httpMethod !== 'POST') {
    return { statusCode: 404 }
  }

  // Check signing signature
  const sig = event.headers['stripe-signature']
  let stripeEvent
  try {
    stripeEvent = stripe.webhooks.constructEvent(event.body, sig, webhookSecret)
  } catch (err) {
    // On error, log and return the error message
    // console.log(`Webhook Error message: ${err.message}`)
    return {
      statusCode: 400,
      body: `Event Error: ${err.message}`,
    }
  }

stripe.webhooks.constructEvent wants event.body to be passed into it unparsed, (works fine on dev) but when pushed to production on Vercel. It seems some kind of processing is happening to the event.body meaning that stripe will not accept it.

to what I understand is that stripe wants rawJSON but something is parsing that JSON.

this would be the solution if bodyParser was used with express

app.use(bodyParser.json({
    // Because Stripe needs the raw body, we compute it but only when hitting the Stripe callback URL.
    verify: function(req,res,buf) {
        var url = req.originalUrl;
        if (url.startsWith('/stripe-webhooks')) {
            req.rawBody = buf.toString()
        }
    }}));
@peterp
Copy link
Contributor

peterp commented Oct 28, 2020

Interesting. This could be our fault. I think we should determine what AWS lambda is doing with the event.body, are they converting it to JSON? Or are they leaving it raw?

@BurnedChris
Copy link
Contributor Author

@BurnedChris
Copy link
Contributor Author

I tried running both of these and it did not work! still errors on vercel production

import { stripe } from 'src/lib/stripe'
import { buffer } from 'micro'

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

export const handler = async (event) => {
  // Only allow POST method
  if (event.httpMethod !== 'POST') {
    return { statusCode: 404 }
  }

  const buf = await buffer(event)

  // Check signing signature
  const sig = event.headers['stripe-signature']

  console.log('==========')
  console.log(typeof event.body)
  console.log('==========')

  let stripeEvent
  try {
    stripeEvent = stripe.webhooks.constructEvent(
      buf.toString(),
      sig,
      webhookSecret
    )
  } catch (err) {
    // On error, log and return the error message
    // console.log(`Webhook Error message: ${err.message}`)
    return {
      statusCode: 400,
      body: `Event Error: ${err.message}`,
    }
  }

  // Successfully constructed event
  console.log('Webhook Success:', stripeEvent.id)

  return {
    statusCode: 200,
    body: `Event Success: ${stripeEvent.id}`,
  }
}

I also tried without buff

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

export const handler = async (event) => {
  // Only allow POST method
  if (event.httpMethod !== 'POST') {
    return { statusCode: 404 }
  }

  // Check signing signature
  const sig = event.headers['stripe-signature']

  console.log('==========')
  console.log(typeof event.body)
  console.log('==========')

  let stripeEvent
  try {
    stripeEvent = stripe.webhooks.constructEvent(
      process.env.NODE_ENV === 'production'
        ? JSON.stringify(event.body)
        : event.body,
      sig,
      webhookSecret
    )
  } catch (err) {
    // On error, log and return the error message
    // console.log(`Webhook Error message: ${err.message}`)
    return {
      statusCode: 400,
      body: `Event Error: ${err.message}`,
    }
  }

  // Successfully constructed event
  console.log('Webhook Success:', stripeEvent.id)

  return {
    statusCode: 200,
    body: `Event Success: ${stripeEvent.id}`,
  }
}

@BurnedChris BurnedChris changed the title Vercel Production + Stripe Web Hooks = breaking bug! Possibly bodyParser Problems Vercel Production API bodyParser (stripe webhooks) Oct 28, 2020
@thorwebdev
Copy link

@burnsy can you confirm that you've enabled https://vercel.com/docs/runtimes#advanced-usage/advanced-node-js-usage/aws-lambda-api this (I guess so, because I'd imagine otherwise Redwood wouldn't run on vercel?)

Interesting. This could be our fault. I think we should determine what AWS lambda is doing with the event.body, are they converting it to JSON? Or are they leaving it raw?

With AWS lambda, event.body should be a JSON string of the request payload, so for example on Netlify, the following works:

exports.handler = async ({ body, headers }) => {
  try {
    const stripeEvent = stripe.webhooks.constructEvent(
      body,
      headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET
    );
// [...]

I wonder if Vercel is not confirming to this spec. @leerob would you happen to know the shape of event.body when running in aws-lambda-api mode?

Maybe try disabling the Node.js helpers: https://vercel.com/docs/runtimes#advanced-usage/advanced-node-js-usage/disabling-helpers-for-node-js

@BurnedChris
Copy link
Contributor Author

BurnedChris commented Oct 29, 2020

hey @thorwebdev,

{
  "version": 2,
  "regions": ["lhr1"],
  "github": {
    "enabled": true
  },
  "build": {
    "env": {
      "NODEJS_HELPERS": "0",
      "NODEJS_AWS_HANDLER_NAME": "handler"
    }
  }
}

it still does not work after these config changes.

Event 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

maybe the answer is in here @vercel/redwood. When I have looked for it I could not find the code.

@paulogdm
Copy link

paulogdm commented Nov 2, 2020

@styfle Investigated this issue on our side:

We fixed upstream deps in redwood to make sure it worked well with our functions. Previously, we couldn't even get the graphql endpoint to parse the body. But we fixed that in redwood 0.15.0.
My first thought was this regressed, so I tried deploying 0.20.0 but it also works as expected.
So my next best guess is that this is a Stripe bug.
0.15.0: https://redwood-new-pqzqdi0lz.vercel.app/api/graphql
0.20.0: https://redwood20-lg10fa367.vercel.app/api/graphql

@kyrcha
Copy link

kyrcha commented Apr 22, 2021

I arrived in this issue through Google search and managed to get stripe webhooks working in vercel through this blog post https://maxkarlsson.dev/blog/2020/12/verify-stripe-webhook-signature-in-next-js-api-routes/.

@alec-chernicki
Copy link

alec-chernicki commented Apr 22, 2021

@burnsy I had this exact same issue with all my functions hosted on Vercel. I found that when working with Redwood locally the body of a function is decoded, however, once deployed to Vercel, the body may be base64 encoded. I have the following code in my Stripe webhook function to handle both local development and Vercel:

export const handler = async (req, context) => {
  // Retrieve the event by verifying the signature using the raw body and secret.
  let event;

  const parsedBody = req.isBase64Encoded
    ? Buffer.from(req.body, 'base64').toString('utf-8')
    : req.body;

  try {
    event = stripeClient.webhooks.constructEvent(
      parsedBody,
      req.headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.log(err);
    console.log(`⚠️  Webhook signature verification failed.`);
    console.log(`⚠️  Check the env file and enter the correct webhook secret.`);

    return {
      statusCode: 400,
    };
  }

  ...
};

Hope that helps as well!

@thedavidprice
Copy link
Contributor

Thanks, everyone, for the help here!!

@thedavidprice
Copy link
Contributor

fwiw — this Issue is a good example of something that can make for a great how-to forum post as well.

https://community.redwoodjs.com

@dthyresson
Copy link
Contributor

@burnsy and @AlecOrtega my webhook verifiers jut got merged into main for the upcoming v0.31 release:

verifyEvent
https://github.com/redwoodjs/redwood/blob/main/packages/api/src/webhooks/index.ts

and this is the Stripe verifier that is a timestamp + scheme:

https://github.com/redwoodjs/redwood/blob/main/packages/api/src/auth/verifiers/timestampSchemeVerifier.ts

I've been testing locally and on Netlify but not yet on Vercel -- and this one I had not yet tested with a real Stripe webhook.

It should be available in a canary release and this is an example of a Discourse webhook verification that uses the sha256 verifier (like GitHub uses):

import { verifyEvent, VerifyOptions } from '@redwoodjs/api/webhooks'

import { logger } from 'src/lib/logger'

/**
 * The handler function is your code that processes http request events.
 * You can use return and throw to send a response or error, respectively.
 *
 * Important: When deployed, a custom serverless function is an open API endpoint and
 * is your responsibility to secure appropriately.
 *
 * @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations}
 * in the RedwoodJS documentation for more information.
 *
 * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent
 * @typedef { import('aws-lambda').Context } Context
 * @param { APIGatewayEvent } event - an object which contains information from the invoker.
 * @param { Context } context - contains information about the invocation,
 * function, and execution environment.
 */

export const handler = async (event, _context) => {
  logger.info('Invoked discourseWebhook function')

  try {
    const options = {
      signatureHeader: 'X-Discourse-Event-Signature',
    } as VerifyOptions

    verifyEvent('sha256Verifier', {
      event,
      secret: 'i-keep-this-a-secret',
      options,
    })
  } catch (error) {
    return {
      statusCode: 401,
    }
  }

  logger.debug({ headers: event.headers }, 'Headers')

  logger.debug({ payload: JSON.parse(event.body) }, 'Body payload')

  // do something with payload

  return {
    statusCode: 200,
    body: JSON.stringify({
      data: 'discordWebhook function',
    }),
  }
}

For Stripe, but swap out:

    const options = {
      signatureHeader: 'stripe-signature',
    } as VerifyOptions

    verifyEvent('timestampSchemeVerifier', {
      event,
      secret: process.env.STRIPE_WEBHOOK_SECRET,
      options,
    })

I hadn't run across the base64case yet, but I could see having to do that here:

https://github.com/redwoodjs/redwood/blob/main/packages/api/src/webhooks/index.ts#L73

Is there a way you could help me test this?

@alec-chernicki
Copy link

@dthyresson Absolutely! I can try this out with the hooks I've seen this issue, deploy it up to a preview environment, and see if I see the above issue or if things come through. I'll try to get to it tomorrow evening or the following day.

@dthyresson
Copy link
Contributor

Absolutely! I can try this out with the hooks I've seen this issue, deploy it up to a preview environment,

Thanks! After review, I think I am going to add the base64 conversion in the verifyEvent to be safe but prob tomorrow.

@jhash
Copy link

jhash commented Sep 8, 2023

@AlecOrtega you just saved my life with that isBase64Encoded check on vercel. Thank you so much 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants