How can I access the raw body of a request for Webhook signature-comparison purposes?

Hey all,

I’m currently creating a Pipedream workflow to take sales from Lemon Squeezy and add the buyer email to a ConvertKit form.

Lemon Squeezy recommends verifying the hashed signature sent in the payload’s header here: Webhooks: Lemon Squeezy

My problem: I cannot figure out how to access the “raw body” referenced in their Node.js sample code. (seen on Line 5 - “const digest”)

const crypto = require('crypto');

const secret    = '[SIGNING_SECRET]';
const hmac      = crypto.createHmac('sha256', secret);
const digest    = Buffer.from(hmac.update(request.rawBody).digest('hex'), 'utf8');
const signature = Buffer.from(request.get('X-Signature') || '', 'utf8');

if (!crypto.timingSafeEqual(digest, signature)) {
    throw new Error('Invalid signature.');
}

I understand all other parts of this code, but the request.rawBody part is where I’m stuck.

As I understand it, I don’t need to (and likely can’t) use request library, since Pipedream already received the payload in the initial step within my workflow and returned a JSON object.

I’ve tried JSON.stringify(steps.trigger.event.body), but that doesn’t seem to work. The hash doesn’t match the one sent in the x-signature of the header.

Is there another way I can hash the raw body of the payload in a manner identical to how Lemon Squeezy is doing it on their end?

Here’s the entire code of the step I’m building in case it’s helpful:

import crypto from 'crypto'

export default defineComponent({
  async run({ steps, $ }) {

    const secret = '[SECRET KEY]'
    const algo = 'sha256'

    const hmac = crypto.createHmac(algo, secret)

    hmac.update(JSON.stringify(steps.trigger.event.body))

    const crypt = hmac.digest('hex')
    const result = Buffer.from(crypt)
    console.log(crypt)
    console.log(result)
    const sig = Buffer.from(steps.trigger.event.headers["x-signature"])
    console.log(steps.trigger.event.headers["x-signature"])
    console.log(sig)

    if (crypto.timingSafeEqual(result, sig)) {
      console.log("Hashes match.")
    } else {
      console.log("Hashes do not match.")
    }
  },
})

Hello @thomas,

First off, welcome to Pipedream! Happy to have you!

You can select the Raw request option in your HTTP Webhook to access all fields of the request

May I ask why do you need to use rawBody instead of the steps.trigger.body field?

Thank you! I’ll try it that way.

I’ve already tried with steps.trigger.body:

hmac.update(JSON.stringify(steps.trigger.event.body))

Unfortunately that didn’t produce a hash match. So it’s either steps.trigger.event.body that’s causing the issue, or it’s using JSON.stringify. And Lemon Squeezy’s docs don’t really explain exactly how they’re hashing the payload when they send it, other than demonstrating that request.rawBody code sample.

Hi @thomas

I have a bit of a rhyming example from another API that might help.

Here’s the source code of our Shopify Partner webhook verification action:

Short video on how to configure the Pipedream HTTP webhook trigger to use a raw body:

The appSecretKey prop in this example code might align with your LemonSqueezy secret, that is if they’re also using HMAC for signing the request.

Raw request worked!

Now I just need to turn the raw request back into a JSON object so I can grab relative paths for the rest of the workflow.

Thanks for your help!

1 Like

Awesome :slight_smile:

If you want help adding your verify Lemon Squeezy webhook action to the official integration just let me know.

We’d be happy to add your action to the public registry so you and other Pipedreamers can reuse it without recoding it.

I’ve try the same thing but it doesn’t look like steps.trigger.event.body return the body in a raw format. I’ve test the webhook on webhook.site and copy/paste the body in a hmac generator with my app client secret. It gives me the good hash but when I try with the output of JSON.stringify, it doesn’t work. I can’t pass the body directly steps.trigger.event.body because it’s an object…

Hi @olivier

Could you take a look at this source code, in particular this line? pipedream/verify-webhook.mjs at ad182a0a84fe7c56c475fa99bd8ef4d6ac39d774 · PipedreamHQ/pipedream · GitHub

You can see how we stringify the raw body object, then apply the HMAC signature.

I’ve already try this and it doesn’t work because JSON.stringify() change the indentation. The only way I got it to work was by recreating the body manually in a string and replacing the values with variables like this.

const body = `{
"shop_id": ${steps.trigger.event.body.shop_id},
"shop_domain": "${steps.trigger.event.body.shop_domain}",
"orders_requested": ${JSON.stringify(steps.trigger.event.body.orders_requested).replace(/,/g, ', ')},
"customer": {
  "id": ${steps.trigger.event.body.customer.id},
  "email": "${steps.trigger.event.body.customer.email}",
  "phone":  "${steps.trigger.event.body.customer.phone}"
},
"data_request": {
  "id": ${steps.trigger.event.body.data_request.id}
}
}`;

Hi @olivier

Indentation doesn’t affect JSON parsing or stringification.

Could you please start a new thread and provide more context on the issue you’re trying to solve for?

Please include screenshots and code samples to help us see how you have your workflow currently configured.

Here’s how I’ve done it:

First set the trigger to capture the raw request, then copy the path of the body:

In the next step, set things up like this:

import crypto from 'crypto'

export default defineComponent({
  async run({ steps, $ }) {

    const secret = '[SECRET KEY HERE]'
    const algo = 'sha256'

    const hmac = crypto.createHmac(algo, secret)

    hmac.update(steps.trigger.event.body)

    const crypt = hmac.digest('hex')
    const result = Buffer.from(crypt)
    console.log(crypt)
    console.log(result)
    const sig = Buffer.from(steps.trigger.event.headers[11])
    console.log(steps.trigger.event.headers[11])
    console.log(sig)

    if (crypto.timingSafeEqual(result, sig)) {
      console.log("Hashes match.")
      const hashMatch = "Match"
      return hashMatch
    } else {
      console.log("Hashes do not match.")
      throw new Error('Invalid hash.')
    }
  },
})

From there, I parse the body as JSON so I can get a workable object for further steps (in my case, subscribing the buyer to a ConvertKit form, and sending a log of the sale to Slack)

Hi @olivier,

request.rawBody in the LemonSqueezy documentation is the same format as steps.trigger.event.body when you have the HTTP webhook configuration set to Raw Request:

What they’re really saying is, request.rawBody is the payload body as a JSON string not as an JS object.

I tried it myself, with slightly different code:

import crypto from 'crypto'

// To use previous step data, pass the `steps` object to the run() function
export default defineComponent({
  props: {
    secret: {
      type: 'string',
      label: "LemonSqueezy Webhook Secret",
      description: "The secret from the LemonSqueezy webhook dashboard",
    }
  },
  async run({ steps, $ }) {
    const secret    = this.secret;
    const hmac      = crypto.createHmac('sha256', secret);
    const digest    = Buffer.from(hmac.update(steps.trigger.event.body).digest('hex'), 'utf8');
    const signature = Buffer.from(steps.trigger.event.headers[11] || '', 'utf8');

    if (!crypto.timingSafeEqual(digest, signature)) {
      $.flow.exit('Invalid webhook signature')
    }

    return true
  },
})

Then after the webhook has been verified, then parse the raw body aka JSON string back into an object:

// in a downstream Node.js code step:
export default defineComponent({
  async run({ steps, $ }) {
    // Parse the "raw body" back into an Object for easier use.
    return JSON.parse(steps.trigger.event.body)
  },
})

Does this answer your question?

Thanks a lot every one! I was able to make it work when I changed my request to raw request.

1 Like