Shopify / shopify-app-template-node

MIT License
875 stars 395 forks source link

Could not validate request HMAC #1296

Open yanglee2421 opened 1 year ago

yanglee2421 commented 1 year ago

Issue summary

I am building a merchant-facing app to onboard merchants to my marketplace. After setting up shopifyApp from @shopify/shopify-app-express, the app it keeps giving the following error:

Webhook request is invalid, returning 401: Could not validate request HMAC
Failed to process webhook: Error: Could not validate request HMAC

Complete setup (removed key and secret to paste here):

// Shopify.ts
export const shopify = shopifyApp({
  api: {
    apiVersion: LATEST_API_VERSION,
    restResources,
    ...env(),
  },
  auth: {
    path: "/api/auth",
    callbackPath: "/api/auth/callback",
  },
  webhooks: {
    path: "/api/webhooks",
  },
  sessionStorage: new SQLiteSessionStorage(DB_PATH),
});

// GDPR.ts
import { DeliveryMethod } from "@shopify/shopify-api";
import { WebhookHandlersParam } from "@shopify/shopify-app-express";

export const webhookHandlers: WebhookHandlersParam = {
  CUSTOMERS_DATA_REQUEST: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/api/webhooks",
    async callback(topic: any, shop: any, body: any, webhookId: any) {
      const payload = JSON.parse(body);
      console.log("CUSTOMERS_DATA_REQUEST", webhookId);
    },
  },

  CUSTOMERS_REDACT: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/api/webhooks",
    async callback(topic: any, shop: any, body: any, webhookId: any) {
      const payload = JSON.parse(body);
      console.log("CUSTOMERS_REDACT", webhookId);
    },
  },

  SHOP_REDACT: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/api/webhooks",
    async callback(topic: any, shop: any, body: any, webhookId: any) {
      const payload = JSON.parse(body);
      console.log("SHOP_REDACT", webhookId);
    },
  },

  APP_UNINSTALLED: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/api/webhooks",
    async callback(topic: any, shop: any, body: any, webhookId: any) {
      const payload = JSON.parse(body);
      console.log("APP_UNINSTALLED", webhookId);
    },
  },

  PRODUCTS_CREATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/api/webhooks",
    async callback(topic: any, shop: any, body: any, webhookId: any) {
      const payload = JSON.parse(body);
      console.log("PRODUCTS_CREATE", webhookId);
    },
  },

  PRODUCTS_DELETE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/api/webhooks",
    async callback(topic: any, shop: any, body: any, webhookId: any) {
      const payload = JSON.parse(body);
      console.log("PRODUCTS_DELETE", webhookId);
    },
  },
};

Expected behavior

Actual behavior

Giving error:

Webhook request is invalid, returning 401: Could not validate request HMAC
Failed to process webhook: Error: Could not validate request HMAC

Steps to reproduce the problem

  1. git clone https://github.com/yanglee2421/shopify-app-demo.git
  2. yarn && yarn dev
  3. yarn shopify webhook trigger
ernesto-ck commented 12 months ago

Hi, any update on this, I have the same issue and I tried different solutions but nothing works

yanglee2421 commented 12 months ago

Unfortunately, there have been no new discoveries

从 Windows 版邮件https://go.microsoft.com/fwlink/?LinkId=550986发送

发件人: Ernesto La @.> 发送时间: 2023年10月10日 13:47 收件人: @.> 抄送: @.>; @.> 主题: Re: [Shopify/shopify-app-template-node] Could not validate request HMAC (Issue #1296)

Hi, any update on this, I have the same issue and I tried different solutions but nothing works

― Reply to this email directly, view it on GitHubhttps://github.com/Shopify/shopify-app-template-node/issues/1296#issuecomment-1754431599, or unsubscribehttps://github.com/notifications/unsubscribe-auth/A5GNBTG24LSNTJJFZKX4BMDX6TOOTAVCNFSM6AAAAAA4G52KW2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTONJUGQZTCNJZHE. You are receiving this because you authored the thread.Message ID: @.***>

rameardo commented 2 months ago

To ensure that a webhook was sent from Shopify, you need to verify its authenticity by calculating a digital signature. Shopify includes a header x-shopify-hmac-sha256 with the request, which contains a base64-encoded HMAC-SHA256 hash of the request body. This hash is generated using the secret key you set in your webhook settings.

You can find more details here.

Here's an improved example in JavaScript:

function verifyWebhook(data, hmacHeader, CLIENT_SECRET) {
  const encoder = new TextEncoder();
  const keyData = encoder.encode(CLIENT_SECRET);

  return crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
    .then(key => crypto.subtle.sign('HMAC', key, data))
    .then(signature => {
      const calculatedHmac = btoa(String.fromCharCode(...new Uint8Array(signature)));
      return calculatedHmac === hmacHeader;
    });
}

const clonedRequest = request.clone()
const hmac = clonedRequest.headers.get('x-shopify-hmac-sha256')

if (!hmac)
  return new Response('not shopify request', { status: 401 })

const CLIENT_SECRET = "YOUR_SHOPIFY_SECRET"
const body = await clonedRequest.arrayBuffer()
const isValid = await verifyWebhook(body, hmac, CLIENT_SECRET)

if (!isValid)
  return new Response('Invalid Signature/HMAC', { status: 401 })

// Continue with your logic

// return shopify 200 response
return new Response('OK', { status: 200 })

This code performs the following steps:

  1. Imports the client secret key for HMAC signing.
  2. Signs the request body with the imported key.
  3. Converts the signature to a base64 string.
  4. Compares the calculated HMAC with the HMAC received in the request header.
  5. Returns a 401 response if the HMAC verification fails or a 200 response if it succeeds.

I hope this helps! Let me know if you have further questions.