Search code examples
node.jswebhookshmacshopify-app

Shopify HMAC verification failing in webhook implementation


I'm working on developing a Shopify app. I'm trying to verify the Shopify transaction that happened on the app using Shopify's HMAC authentication verification using this function. The webhook is created through the Shopify Webhook API for the topic app_subscriptions/update and shopifyApiSecret is the Client Secret for our Shopify app. Here's the snippet of the code we're using,

async function validateHmac(req: Request) {
  let shopifyApiSecret = config.shopifyAppSecret;

  let hmac: any = req.headers["x-shopify-hmac-sha256"];
  const message = JSON.stringify(req.body);
  const generatedHash = crypto
    .createHmac("sha256", shopifyApiSecret)
    .update(message)
    .digest("base64");
  console.log({ message, generatedHash, hmac });

  const signatureOk = crypto.timingSafeEqual(
    Buffer.from(generatedHash),
    Buffer.from(hmac)
  );
  if (signatureOk) {
    return true;
  } else {
    return false;
  }
}

We've tried comparing both the ways, that is with === as well as using timingSafeEqual, but the function always returns false and the generatedHash and hmac are not equal on inspection. Can anyone let me know if there is anything wrong with this implementation? Thanks in advance.


Solution

  • The issue was with the data we were consuming to generate the message. We were able to solve it by using the express function in our router

    express.json({
      limit: '10mb',
      verify: (req, _res, buf) => {
        (req as any).rawBody = buf;
      },
    });
    

    and passing the rawBody to the update function.

    async function validateHmac(req: Request) {
      let shopifyApiSecret = config.shopifyAppSecret;
    
      let hmac: any = req.headers["x-shopify-hmac-sha256"];
      const message = req.rawBody;
      const generatedHash = crypto
        .createHmac("sha256", shopifyApiSecret)
        .update(message)
        .digest("base64");
      console.log({ message, generatedHash, hmac });
    
      const signatureOk = crypto.timingSafeEqual(
        Buffer.from(generatedHash),
        Buffer.from(hmac)
      );
      if (signatureOk) {
        return true;
      } else {
        return false;
      }
    }