How do I get raw body in my Lambda?

Hi.

I’m trying to use signature on my Stripe webhooks. In Stripe’s docs they’re using body-parser with Express (bodyParser.raw({type: 'application/json'})). I searched for the issue and best to my research (github issue I can do it in Lamda provided I pass raw body along with the signature and my secret.

As described in the GitHub issue, I need to map the endpoint from AWS admin console. From there, there is a way to set up Body Mapping Template and include rawbody": "$util.escapeJavaScript($input.body)", which will then let me use rawbody in my Lambda.

How can I create a mapping like this with Netlify, or is there an alternative to get raw body? I understand it needs to be exactly the way Stripe sent it to my endpoint, passed back on to Stripe.

Cheers

Hi @abtx, It’s not possible to add any custom configuration to your function since it’s not deployed to your AWS account. I have not looked in to this before but just to clarify, have you tried using the event.body string?

Hi @futuregerald , yes, I did. Stripe API requires raw body specifically. Is there any way Netlify will enable it or has plans for AWS like config in the functions?

@abtx, there aren’t any plans to allow passing the raw body into functions, but it is possible to use your own aws lambda account if you have a custom netlify plan, but even with this, I don’t know if the raw body will be sent properly since we do use our own proxy between the request and the lambda function. This is likely a use-case that you can’t do with Netlify in the near future. Sorry about that!

That’s a shame. I hope I can find another way to verify webhooks then, but probably not as secure. Thanks for the reply.

1 Like

Hi there @abtx, did you find any solutions to this problem?

It’s Stripe SCA for Europe that’s requiring me to upgrade to PaymentIntents and webhooks. Checkout is going to introduce a load of headaches if I have to switch to using that rather than the Netlify Functions set up currently!

I’ve looked at manually verifying webhook event signatures but suspect this is going to run into the same problem.

@hawks no, I’ve not done that yet. I’m using checkout by default right now and use Netlify functions to trigger charge succeeded hook at the moment. I’m yet to implement the logic to verify I’ve got the stock before the charge takes place, and I want to use the hook to trigger the order placing logic. The hook will succeed anyway, and I’m not 100% sure how will I verify it came from Stripe yet if I can’t do it with raw body. As far as I understand, the checkout is SCA ready, which means I will be able to charge the customer nonetheless, and it won’t affect the ability to actually charge the customer, but I might expose myself to an attack that will trigger fake orders if I don’t find a way to authenticate the hook trigger if that makes sense. How are you using Netlify functions at the moment if you’re not using Stripe checkout?

@abtx Thanks for replying. I think you are all correct there, I quickly rolled my own verification (using the method outlined in the Stripe link I posted) and the signatures match when I run test events from the Stripe Dashboad & CLI, I can chuck some (rough) code up if you like later.

I’ve been using Netlify Functions as part of a workflow that used the old Orders API/Token in Stripe Elements and thus is not SCA ready.

When my project originally started, Checkout was a lot more barebones and I couldn’t use it, now it seems like it is an option but the manual verification seems to have worked! I need to understand why Stripe wanted the raw body and test more to make sure as you say there’s no potential attack surface.

Oh and just to add further context for anyone interested: I’m refactoring from the Orders API to the PaymentIntents API

@Hawks does it matter what API you’re using if one way or another you’ll trigger a webhook anyway? I assume you could listen for invoice.payment_succeeded or payment_intent.succeeded and use manual verification with it? If you can share your code here later that’d be great.

Stripe say

Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature.

I thought that Stripe would call the hook sending timestamped rawbody which you send back and they compare it to what they’ve sent, but I need to find out for sure.

With my original code using the Orders API, there was no need to use webhooks, everything was handled via the Stripe servers and then their response to the Netfliy Function was simply sent to the client so no verification required.

With PaymentIntent, I have to use the webhooks for handle fulfillment so that’s why I’m at this point and as you’ve pointed out: it is something I will have to worry about any with the Checkout as I can’t use the success/fail urls for fulfillment and will have to use webhooks.

Here’s the verification I had a shot at:

exports.handler = async (event, context) => {
if (!event.body || event.httpMethod !== "POST") {
    return errorReport("invalid-method")
}

// https://stripe.com/docs/webhooks/signatures#verify-manually
// 1. Extract timestamp and sigs from header
const sig = event.headers["stripe-signature"]
const pairs = sig.split(",").map(pair => pair.split("="))

// 1a. Key/Value pair assignment
const fromEntries = arr =>
    Object.assign({}, ...Array.from(arr, ([k, v]) => ({ [k]: v })))
const entries = fromEntries(pairs)

// 2. Prepare the signed_payload string
const timestamp = entries.t
const signedPayload = `${timestamp}.${event.body}`
// TODO: Error handling from bad header/entries

// 3. Determine the expected signature
const hmac = crypto
    .createHmac("sha256", endpointSecret)
    .update(signedPayload)
    .digest("hex")

// 4. Compare sigs and evaluate timestamps
// Should match against any of the stamps here? (v0, v1 etc)
// 4a. Signature match
const matches = crypto.timingSafeEqual(
    Buffer.from(hmac, "hex"),
    Buffer.from(entries.v1, "hex")
)
// 4b. Timestamp difference within 5 mins
const current = Math.floor(+new Date() / 1000)
const diff = Math.abs(current - timestamp)
const exceeded = diff > 60 * 5
if (exceeded) {
    return errorReport("bad-timing")
}
if (!matches) {
    return errorReport("bad-signature")
}

const stripeEvent = JSON.parse(event.body)
let intent = null
let message = ""
switch (stripeEvent["type"]) {
    case "payment_intent.succeeded":
    intent = stripeEvent.data.object
    message = `Succeeded: ${intent.id}`
    break
    case "payment_intent.payment_failed":
    intent = stripeEvent.data.object
    message = `Failed: ${intent.id}, ${intent.last_payment_error &&
        intent.last_payment_error.message}`
    break
}

return paymentReport(message)
}

Let me know what you think.

1 Like

It’s pretty rough and could use a refactor I know, it was a quick attempt but it does work with the test events that you can send via the Stripe Dashboard

Just coming back to this after Christmas Hols & time after in January.

To anyone getting to this point:
I ended up using Checkout and the code above works for verifying webhooks although I recommend taking a look at the Offical Stripe Webhook Lib to see how the code above can be refactored.

I should have looked at that in the first place! It’s all working in a live system now so hopefully will work for everyone else too.

1 Like

Thanks for sharing your manual verification code, unfortunately it is not working for me.

I see 3 different hashes for v0, v1 and hmac.

One thing I don’t understand is how the generation of signedPayload in step 2 can work?

const signedPayload = {timestamp}.{event.body};

When I log this out I see:

1590179791.{
  "id": "evt_1GlhbVHypXujNqlR6Edxh3Bc",
  "object": "event",
  "api_version": "2020-03-02",
  "created": 1590179788,
  "data": {
    ...
   }
}

But thought this should be a Buffer or something like that?

Any help greatly appreciated. Seems I can’t avoid using a webhook to verify purchases on Stripe. I will have to have a closer look at their library next.

In case it helps anyone else, as a workaround what I’ve done is rolled my own webhook verification by passing a JWT in the client_reference_id field with the data I need.

This is created on checkout session creation and then verified when webhook is received.

I think this provides a similar level of security for the webhook receiver…

that makes sense. you can then use something like the jsonwebtoken npm package to validate the jwt.:+1: