Nodejs - Expressjs - 验证 shopify webhook

Nodejs - Expressjs - Verify shopify webhook

我正在尝试在开发环境中验证从 shopify webhook 发送的 hmac 代码。但是 shopify 不会向非实时端点发送 post 对 webhook 的请求,因此我使用 requestbin 捕获请求然后使用 postman将其发送到我的本地网络服务器。

来自 shopify documentation, I seem to be doing everything right and have also tried applying the method used in node-shopify-auth verifyWebhookHMAC function。但是到目前为止,none 已经奏效了。 这些代码永远不会匹配。 我在这里做错了什么?

我用来验证 webhook 的代码:

 function verifyWebHook(req, res, next) {
      var message = JSON.stringify(req.body);
    //Shopify seems to be escaping forward slashes when the build the HMAC
        // so we need to do the same otherwise it will fail validation
        // Shopify also seems to replace '&' with \u0026 ...
        //message = message.replace('/', '\/');
        message = message.split('/').join('\/');
    message = message.split('&').join('\u0026');
      var signature = crypto.createHmac('sha256', shopifyConfig.secret).update(message).digest('base64');
      var reqHeaderHmac = req.headers['x-shopify-hmac-sha256'];
      var truthCondition = signature === reqHeaderHmac;

      winston.info('sha256 signature: ' + signature);
      winston.info('x-shopify-hmac-sha256 from header: ' + reqHeaderHmac);
      winston.info(req.body);

      if (truthCondition) {
        winston.info('webhook verified');
        req.body = JSON.parse(req.body.toString());
        res.sendStatus(200);
        res.end();
        next();
      } else {
        winston.info('Failed to verify web-hook');
        res.writeHead(401);
        res.end('Unverified webhook');
      }
    }

我收到请求的路由:

router.post('/update-product', useBodyParserJson, verifyWebHook, function (req, res) {
  var shopName = req.headers['x-shopify-shop-domain'].slice(0, -14);
  var itemId = req.headers['x-shopify-product-id'];
  winston.info('Shopname from webhook is: ' + shopName + ' For item: ' + itemId);
});

我做的有点不同 -- 不确定我在哪里看到的建议,但我在 body 解析器中进行了验证。 IIRC 的一个原因是我在任何其他处理程序可能触及它之前就可以访问原始 body:

app.use( bodyParser.json({verify: function(req, res, buf, encoding) {
    var shopHMAC = req.get('x-shopify-hmac-sha256');
    if(!shopHMAC) return;
    if(req.get('x-kotn-webhook-verified')) throw "Unexpected webhook verified header";
    var sharedSecret = process.env.API_SECRET;
    var digest = crypto.createHmac('SHA256', sharedSecret).update(buf).digest('base64');
    if(digest == req.get('x-shopify-hmac-sha256')){
        req.headers['x-kotn-webhook-verified']= '200';
    }
 }})); 

然后任何网络挂钩只处理经过验证的 header:

if('200' != req.get('x-kotn-webhook-verified')){
    console.log('invalid signature for uninstall');
    res.status(204).send();
    return;
}
var shop = req.get('x-shopify-shop-domain');
if(!shop){
    console.log('missing shop header for uninstall');
    res.status(400).send('missing shop');
    return;
}

简答

express 中的正文解析器不能很好地处理 BigInt,并且作为整数传递的订单号之类的东西会被破坏。除了某些值被编辑之外,例如 URL 最初作为 "https://..." 发送,OP 还从其他代码中发现了这一点。

为了解决这个问题,不要使用正文解析器解析数据,而是将其作为原始字符串获取,稍后您可以使用 json-bigint 解析它以确保它的 none已损坏。

长答案

虽然@bknights 的 工作得很好,但重要的是首先要找出发生这种情况的原因。

对于我在 Shopify 的 "order_created" 事件上创建的 Webhook,我发现传递给正文的请求 ID 与我从测试数据中发送的 ID 不同,结果是express 中的主体解析器存在问题,它不能很好地处理大整数。

最终我将一些东西部署到 Google 云函数并且请求已经有了我可以使用的原始主体,但是在我的 Node 测试环境中,我将以下内容作为单独的主体解析器实现,因为使用相同的解析器正文解析器两次用 JSON

覆盖原始正文
var rawBodySaver = function (req, res, buf, encoding) {
    if (buf && buf.length) {
      req.rawBody = buf.toString(encoding || 'utf8');
    }
}
app.use(bodyParser.json({verify: rawBodySaver, extended: true}));

基于this回答

我稍后使用 json-bigint 解析 rawBody 以便在其他地方的代码中使用,否则一些数字会被损坏。

// Change the way body-parser is used
const bodyParser = require('body-parser');

var rawBodySaver = function (req, res, buf, encoding) {
    if (buf && buf.length) {
        req.rawBody = buf.toString(encoding || 'utf8');
    }
}
app.use(bodyParser.json({ verify: rawBodySaver, extended: true }));


// Now we can access raw-body any where in out application as follows
// request.rawBody in routes;

// verify webhook middleware
const verifyWebhook = function (req, res, next) {
    console.log('Hey!!! we got a webhook to verify!');

    const hmac_header = req.get('X-Shopify-Hmac-Sha256');
    
    const body = req.rawBody;
    const calculated_hmac = crypto.createHmac('SHA256', secretKey)
        .update(body,'utf8', 'hex')
        .digest('base64');

    console.log('calculated_hmac', calculated_hmac);
    console.log('hmac_header', hmac_header);

    if (calculated_hmac == hmac_header) {
        console.log('Phew, it came from Shopify!');
        res.status(200).send('ok');
        next();
    }else {
        console.log('Danger! Not from Shopify!')
        res.status(403).send('invalid');
    }

}

有同样的问题。使用 request.rawBody 而不是 request.body 有帮助:

import Router from "koa-router";
import koaBodyParser from "koa-bodyparser";
import crypto from "crypto";

...

koaServer.use(koaBodyParser()); 

...

koaRouter.post(
    "/webhooks/<yourwebhook>",
    verifyShopifyWebhooks,
    async (ctx) => {
      try {
        ctx.res.statusCode = 200;
      } catch (error) {
        console.log(`Failed to process webhook: ${error}`);
      }
    }
);

...

async function verifyShopifyWebhooks(ctx, next) {
  const generateHash = crypto
    .createHmac("sha256", process.env.SHOPIFY_WEBHOOKS_KEY) // that's not your Shopify API secret key, but the key under Webhooks section in your admin panel (<yourstore>.myshopify.com/admin/settings/notifications) where it says "All your webhooks will be signed with [SHOPIFY_WEBHOOKS_KEY] so you can verify their integrity
    .update(ctx.request.rawBody, "utf-8")
    .digest("base64");

  if (generateHash !== shopifyHmac) {
    ctx.throw(401, "Couldn't verify Shopify webhook HMAC");
  } else {
    console.log("Successfully verified Shopify webhook HMAC");
  }
  await next();
}