Shopify / shopify-api-js

Shopify Admin API Library for Node. Accelerate development with support for authentication, graphql proxy, webhooks
MIT License
942 stars 390 forks source link

Mandantory GDPR webhooks #256

Closed cmnstmntmn closed 2 years ago

cmnstmntmn commented 2 years ago

There is no easy way to find the topic for a mandatory web-hook;

From documentations the list of these 3 mandantory webhooks is:

list 1:

however, all examples includes APP_UNINSTALLED but there is no example with the other three.

My question so far.. Is there a correlation between list 1 and this list https://shopify.dev/api/admin-graphql/2021-10/enums/webhooksubscriptiontopic ?

dani-sanomads commented 1 year ago

Add middleware to each post request for Webhooks to validate the hmac.

ahkjxy commented 1 year ago

Webhooks

like this?

const webhook = receiveWebhook({ secret: SHOPIFY_API_SECRET_KEY, })

// GDPR Webhooks router.post('/webhooks/customers/redact', webhook, async ctx => { ctx.res.statusCode = 200 }) router.post('/webhooks/shop/redact', webhook, async ctx => { ctx.res.statusCode = 200 }) router.post('/webhooks/customers/data_request', webhook, async ctx => { ctx.res.statusCode = 200 })

dani-sanomads commented 1 year ago

make it a proper function for hmac validation.

ahkjxy commented 1 year ago

make it a proper function for hmac validation.

require('isomorphic-fetch') const Koa = require('koa') // const helmet = require("koa-helmet") const { koaBody } = require('koa-body')

const { Shopify } = require('@shopify/shopify-api')

const next = require('next') const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth') const { verifyRequest } = require('@shopify/koa-shopify-auth') const session = require('koa-session') const Router = require('koa-router') const getSubscriptionUrl = require('./utils/getSubscriptionUrl')

const { APP_CONFIG } = require('./config/app.config') const { SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY, PORT } = APP_CONFIG

const port = parseInt(PORT, 10) const dev = process.env.NODE_ENV !== 'production' const app = next({ dev, }) const handle = app.getRequestHandler()

app.prepare().then(() => { const server = new Koa() const router = new Router() server.use( session( { sameSite: 'none', secure: true, }, server ) ) server.keys = [SHOPIFY_API_SECRET_KEY]

server.use( createShopifyAuth({ apiKey: SHOPIFY_API_KEY, secret: SHOPIFY_API_SECRET_KEY, scopes: ['read_products', 'read_orders'], async afterAuth(ctx) { const { shop, accessToken } = ctx.session ctx.cookies.set('shopOrigin', shop, { httpOnly: false, secure: true, sameSite: 'none', })

    ctx.cookies.set('accessToken', accessToken, {
      httpOnly: false,
      secure: true,
      sameSite: 'none',
    })

    await getSubscriptionUrl(ctx, accessToken, shop)

    const response = await fetch(
      `https://${shop}/admin/api/2019-07/shop.json`,
      {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'X-Shopify-Access-Token': accessToken,
        },
      }
    )
    const shopId = response.headers._headers['x-shopid']
    ctx.cookies.set('shopId', shopId, {
      httpOnly: false,
      secure: true,
      sameSite: 'none',
    })

  },
})

)

function verifyWebhookRequest(body) { try { const generatedHash = crypto .createHmac("SHA256", Shopify.Context.API_SECRET_KEY) .update(JSON.stringify(body), "utf8") .digest("base64"); const hmac = req.get(ShopifyHeader.Hmac); // Equal to 'X-Shopify-Hmac-Sha256' at time of coding const safeCompareResult = Shopify.Utils.safeCompare(generatedHash, hmac); if (!!safeCompareResult) { return true; } else { return false; } } catch (error) { return false; } }

// GDPR Webhooks router.post('/webhooks/customers/redact', koaBody(), async ctx => { if (verifyWebhookRequest(ctx.request.body) === true) { ctx.res.statusCode = 200; } else { ctx.res.statusCode = 401; } }) router.post('/webhooks/shop/redact', koaBody(), async ctx => { if (verifyWebhookRequest(ctx.request.body) === true) { ctx.res.statusCode = 200; } else { ctx.res.statusCode = 401; } }) router.post('/webhooks/customers/data_request', koaBody(), async ctx => { if (verifyWebhookRequest(ctx.request.body) === true) { ctx.res.statusCode = 200; } else { ctx.res.statusCode = 401; } })

/**

vineetIcodelabs commented 1 year ago

import crypto from "crypto"; import { Shopify } from "@shopify/shopify-api";

export const hmacVerify = (req, res, next) => { try { const generateHash = crypto .createHmac("SHA256", process.env.SHOPIFY_API_SECRET) .update(JSON.stringify(req.body), "utf8") .digest("base64");

const hmac = req.headers["x-shopify-hmac-sha256"];

if (Shopify.Utils.safeCompare(generateHash, hmac)) {
  console.log("HMAC successfully verified for webhook route.");
  next();
} else {
  console.log("Shopify hmac verification for webhook failed, aborting.");
  return res.status(401).send();
}

} catch (error) { console.log("--> HMAC ERROR", error); } };

I have used this function to validate webhooks requests. But both generateHash and hmac are always different in my case. Am I missing something?