stripe-node: Problem parsing body: No signatures found matching the expected signature for payload

I’m using AWS API Gateway/Lambda which passes the request body as an actual JSON object to my Lambda function, where I am calling stripe.webhooks.constructEvent on it. It seems this function expects the payload to be stringified JSON, so I run JSON.stringify on the request body, but this fails to sign (I’m guessing because JSON.stringify subtly changes what the JSON string would look like coming from the server). Is it possible for constructEvent to detect if the payload is a JSON object and skip parsing it? I mean, in a way that maintains security?

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 50 (9 by maintainers)

Most upvoted comments

for those who use serverless framework:

your function should look like this.

functions:
  webhook:
    handler: Webhook/index.handler
    timeout: 30
    events:
      - http:
          path: billing/webhook
          method: POST
          integration: lambda
          cors: true
          request:
            template:
              application/json:
                '{"rawBody": "$util.base64Encode($input.body)",
                  "headers": {
                    #foreach($param in $input.params().header.keySet())
                    "$param": "$util.escapeJavaScript($input.params().header.get($param))"
                    #if($foreach.hasNext),#end
                    #end
                }}'

After your create your function, you need to parse base64 and convert to utf8 string;

const raw = Buffer.from(rawBody, 'base64').toString('utf8');

Now you can use it for verification

const stripeEvent = stripe.webhooks.constructEvent(
  raw,
  headers['Stripe-Signature'],
  process.env.whk,
);

I had this same issue. If using lambda proxy (the default/recommended) integration - you need to simply pass the event.body object to the function - WITHOUT ANY PARSING/BUFFER STEPS. It is already stringified… The rest of the object is not, but the body is.

  module.exports.hello = (event) => {
    stripe.webhooks.constructEvent(event.body, sig, endpointSecret)
  }

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.

TL;DR make sure you are using the signing secret from the Stripe dashboard (either test or live mode), and not the secret obtained from the CLI with stripe listen --forward-to xxx. They are different.

I had the same issue for 2 days and tried the solutions in this thread, but it had nothing to do with the API GW body parsing. I was using the signing secret returned by the Stripe CLI (when doing stripe listen --forward-to), and testing locally (with the serverless-offline plugin) worked perfectly, so I went live on my staging Lambda and there the signing failed - because the secret was not the right one ofc.

Using the Serverless framework, and API Gateway with a LAMBDA integration - so no need to set a request template.

// serverless.yaml

...
functions:
  webhook:
    name: projectname-stage-webhook
    description: xxx
    handler: path/to/handler
    role: webhookFunctionRole
    events:
      - http:
          path: webhook
          method: post
          cors: false
...
// handler.ts

const rawBody = event.body;
const requestSignature = event.headers['Stripe-Signature']; // mind the casing
const webhookSecret = 'whsec_xxx', // make sure this is coming from the Stripe dashboard when deploying to your Lambda

try {
  return stripe.webhooks.constructEvent(
    rawBody,
    requestSignature,
    webhookSecret,
  );
} catch (error) {
  log.error('Webhook signature verification failed.', {
    error,
    request: {
      body: rawBody,
      signature: requestSignature,
    },
  });
  throw new StatusCodeError(400, 'Signature verification failed');
}

@chaunceyau This did not work for me 😦

An easy solution is to get the id from the payload and then retrieve the event from stripe:

const payload = JSON.parse(event.body);
const stripeEvent = await stripe.events.retrieve(payload.id);

That way you can trust the event as well.

I have this issue too, after long investigation it caused by my middleware.

Unfortunately I use middleware middy jsonBodyParser, and it caused event.body which is formatted string json, becomes json object.

If you want to debugging the error, I suggest you must see the event.body content. The content has to be EXACTLY SAME as stripe was sent. And the format is “string of formatted json”.

What I mean with “string of formatted json” is formatted string below

'{
  "foo": {
    "bar": 12
  }
}'

// Stripe accept this

is different with this

'{"foo": {"bar": 12}}'

// Stripe not accept this

@redouane-dev you are right.

I had the same issue when I used the signing key from the stripe CLI instead of the signing key from the webhooks dashboard on stripe.

I’m using AWS Amplify with AWS Serverless Express via API Gateway to Lambda using Lambda Integration.

Hope someone else finds this userful.

I managed to get webhooks working like this:

app.use(bodyParser.raw({ type: '*/*' }))

app.post(
  '/webhook',
  (request, response) => {
try {
      event = stripe.webhooks.constructEvent(request.body, request.headers['stripe-signature'], 'whsec...')
    } catch (err) {
      // invalid signature
      console.log('err constructing event: ', err)      
      return response.status(400).send(`Webhook Error: ${err.message}`)
    }
})

@redouane-dev Thank you, you saved me probably hours of googling. It never would’ve occurred to me that I was using the wrong secret. It is too easy to copy-paste settings from local dev to lambda… Thanks!

I have a thorough fix that I figured out with @ambergkim

First configure the mapping template as you all had already discussed

mapping template in api gateway

#set($allParams = $input.params())
{
"raw": "$input.body",
"body-json" : $input.json('$'),
"params" : {
#foreach($type in $allParams.keySet())
    #set($params = $allParams.get($type))
"$type" : {
    #foreach($paramName in $params.keySet())
    "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
        #if($foreach.hasNext),#end
    #end
}
    #if($foreach.hasNext),#end
#end
},
"stage-variables" : {
#foreach($key in $stageVariables.keySet())
"$key" : "$util.escapeJavaScript($stageVariables.get($key))"
    #if($foreach.hasNext),#end
#end
},
"context" : {
    "account-id" : "$context.identity.accountId",
    "api-id" : "$context.apiId",
    "api-key" : "$context.identity.apiKey",
    "authorizer-principal-id" : "$context.authorizer.principalId",
    "caller" : "$context.identity.caller",
    "cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider",
    "cognito-authentication-type" : "$context.identity.cognitoAuthenticationType",
    "cognito-identity-id" : "$context.identity.cognitoIdentityId",
    "cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId",
    "http-method" : "$context.httpMethod",
    "stage" : "$context.stage",
    "source-ip" : "$context.identity.sourceIp",
    "user" : "$context.identity.user",
    "user-agent" : "$context.identity.userAgent",
    "user-arn" : "$context.identity.userArn",
    "request-id" : "$context.requestId",
    "resource-id" : "$context.resourceId",
    "resource-path" : "$context.resourcePath"
    }
}

Then what happens is that event.raw is base64 encoded, so when you try to send that to stripe it will fail.

image

Then in your lambda

// function handler
exports.handler = async (event, context) => {
  // this decodes the b64 into a buffer
  const rawBodyAsBuffer = (new Buffer(event.raw, 'base64'))
  try {
    // just borrowed from stripe docs, obv there are undefined vars in this example
    event = stripe.webhooks.constructEvent(rawBodyAsBuffer, sig, endpointSecret);
  }
  catch (err) {
    response.status(400).send(`Webhook Error: ${err.message}`);
  }
  return {
    statusCode: 200
  }
}

And you should be all set.

@mcrider Don’t forget the single quote escaping, otherwise it will throw 400 Bad Request. https://forums.aws.amazon.com/thread.jspa?messageID=900916&#900916

$util.escapeJavaScript(data).replaceAll("\\'","'")

This function will turn any regular single quotes (‘) into escaped ones ('). However, the escaped single quotes are not valid in JSON. Thus, when the output from this function is used in a JSON property, you must turn any escaped single quotes (') back to regular single quotes (’). This is shown in the following example:

@akanksha1909 this method was added in 4.19.0 as documented in the changelog so it won’t work with your version of stripe-node. You’ll need to upgrade to a newer version for the code to work.

@chaunceyau This did not work for me 😦 An easy solution is to get the id from the payload and then retrieve the event from stripe:

const payload = JSON.parse(event.body);
const stripeEvent = await stripe.events.retrieve(payload.id);

That way you can trust the event as well.

Doing this way you may end up granting the same “purchase” multiple time.

I’m trying to make it works with stripe.webhooks.constructEvent but it’s always failing. With stripe.event.retrieve, it works well.

Someone can explain why it’s not recommended ?

I’m using an API Gateway/Lambda integration and my call is accepting the event.body without any buffering, remapping, or parsing.

exports.handler = async function (event, context, callback) {
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
  const sig = event?.headers['Stripe-Signature'];
  const stripeEvent = stripe.webhooks.constructEvent(event.body, sig, webhookSecret);
  ...

I had this same issue. If using lambda proxy (the default/recommended) integration - you need to simply pass the event.body object to the function - WITHOUT ANY PARSING/BUFFER STEPS. It is already stringified… The rest of the object is not, but the body is.

  module.exports.hello = (event) => {
    stripe.webhooks.constructEvent(event.body, sig, endpointSecret)
  }

Just to elaborate a bit on this solution, I was getting this error Unable to extract timestamp and signatures from header and had to strip the stripe-signature header of [ and ] like this:

module.exports.hello = (event) => {
    const sig = (event.headers["Stripe-Signature"] || "").replace(/\[|]/gi, ""); 
    const stripeEvent = stripe.webhooks.constructEvent(event.body, sig, endpointSecret);
}

Same issue here. I’m using Lambdas via Netlify, and I don’t seem to have the app in AWS Admin where I could set up Body Mapping Templates. Is there any other way I can get the raw response from the event?

@idhard If you reach out to Support - along with more details on your implementation - they can help you with that.

The example is wrong.The endpoint security key should be a buffer type.

let event = stripe.webhooks.constructEvent(req.body, sig, Buffer.from(endpointSecret));

Hi @jlomas-stripe, I fixed the issue a few days ago by resorting to my own custom HMAC SHA comparison as described in the stripe docs. Unfortunately I don’t recollect the issue…