Xero Webhook Validation (Intent to Receive) - Response not 2XX

111 views Asked by At

I'm trying to create a Xero webhook and connect it up to Pipedream but I can't get the "Intent to receive" validation working properly. I've tried a couple of different approaches (using Node.js). This was my first attempt:

import crypto from "crypto";

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

    const key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
          payload = steps.trigger.event.body.toString(),
          // payload = JSON.stringify({"events": [],"lastEventSequence": 0,"firstEventSequence": 0,"entropy": "S0m3r4Nd0mt3xt"}),
          calculatedHmac = crypto.createHmac('sha256', key).update(payload).digest('base64'),
          xeroSignature = steps.trigger.event.headers["x-xero-signature"];

        // console.log(calculatedHmac.trim());
        // console.log(xeroSignature);

    if (calculatedHmac.trim() === xeroSignature) {
      await $.respond({
        status: 200,
        headers: {},
        body: 'ok',
      })
    } else {
      await $.respond({
        status: 401,
        headers: {},
        body: 'unauthorised',
      })
    }
  },
})

And this was my second attempt based on an old GitHub repo I found:

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

    const { createHmac } = await import('crypto');
    const xero_webhook_key = 'xxxxxxxxxxxxxxxxxxxxxxxx'
    const body_string = JSON.stringify(steps.trigger.event.body)
    const xero_hash = steps.trigger.event.headers["x-xero-signature"]

    let our_hash = createHmac('sha256', xero_webhook_key).update(body_string).digest("base64") // Generate the hash Xero wants
    let statusCode = xero_hash == our_hash ? 200 : 401 // If the hashes match, send a 200, else send a 401

    await $.respond({
      status: statusCode
    });

  }
})

Both methods are returning 401 every time, even when a correctly signed signature is sent from Xero. In the first lot of code you'll see I've used console.log to show the calculated HMAC and the Xero Signature, but they never match.

Based on some other threads I found I've also tried using .toString() rather than JSON.stringify(), and tried hardcoding an example payload but none of that worked either.

Any ideas?

1

There are 1 answers

0
DanH On

You have to allow for raw body request type in your API.

This is how I did it using Nest JS:

    //main.ts
     const app = await NestFactory.create(AppModule, {rawBody: true});
    
    //webhook.controller.ts
     @Post()
     async xeroWebhookController (
        @Req() req: RawBodyRequest<Request>,
        @Res() res: Response
      ) {

    const rawPayload = query.rawBody.toString()
    const hmac = crypto.createHmac('sha256', this.config.xeroHashKey).update(rawPayload).digest('base64');
    }

and just regular Express JS

//app.ts
//middleware
this.express.use("/api/3/xero-integrations/xeroWebhook", bodyParser.raw({ type: 'application/json', limit: '50mb' }), xeroWebhookController)

//xeroWebhook.controllers.ts
const hmac = crypto.createHmac('sha256', process.env.XERO_HASH_KEY).update(req.body.toString()).digest('base64');