Nodejs - Expressjs - 验证 Shopify Webhook

5
我正在尝试验证来自开发环境的 Shopify webhook 发送的 hmac 代码。然而,Shopify 不会向非实时端点发送 webhook 的 post 请求,因此我使用 requestbin 捕获请求,然后使用 postman 将其发送到我的本地 web 服务器。
从 Shopify documentation 中,我似乎做了一切正确的事情,并且还尝试了应用 node-shopify-auth verifyWebhookHMAC function 中使用的方法。但是这些都没有起作用。 代码从未匹配过。 我在这里做错了什么?
验证 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);
});
4个回答

7

我会稍微有所不同——不确定我在哪里看到了这个建议,但我将验证放在了body解析器中。如果我没记错的话,其中一个原因是我可以在其他处理程序可能触及它之前获得原始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';
    }
 }})); 

然后任何web钩子都只需处理已验证的标头:

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;
}

此处并未定义 body,实际上是由 body parser 在 verify 后运行时设置的。请使用 buffer。至于测试方面,我只是在应用程序安装时注册了 webhook。由于我的应用程序尚未发布到应用商店,因此我只使用了我的实时 URL。您也可以使用 ngrok 进行测试。 - bknights
我正在使用请求捕获工具来捕获和注册Webhook,然后使用Postman将请求发送给我的开发人员。我想知道这个过程中是否有什么问题!? - hyprstack
1
你计划将实时应用托管在哪里?如果无法使用那个,请尝试使用ngrok替代requestb.in和postman。 - bknights
@bknights 我找不到“sharedSecret”密钥。我只有公共应用程序的秘密密钥。它们是相同的吗?如果不是,我在哪里可以找到它?我在任何地方都没有看到答案。我只看到在管理员下的“Webhooks”设置中显示的密钥,我认为它不用于公共应用程序。如果它实际上是实际密钥,我的公共应用程序将如何从安装了我的应用程序的每个商店获取访问该密钥的位置和方式。也许你可以解开这个谜团。:\ - HymnZzy
是的,公共应用程序的秘密密钥就是“共享密钥”。Shopify使用应用程序的秘密密钥对Webhook进行签名。这与安装应用程序的商店无关。 - bknights
显示剩余6条评论

3

简短回答

Express中的body parser不能很好地处理BigInt,像订单号这样作为整数传递的东西会被损坏。除此之外,某些值也会被编辑,例如URL最初发送为“https://...”,OP也从其他代码中发现了这一点。

为了解决这个问题,不要使用body parser解析数据,而是将其作为原始字符串获取,稍后您可以使用json-bigint解析它,以确保没有任何损坏。

详细回答

尽管@bknights的答案完全有效,但重要的是首先找出为什么会发生这种情况。

对于我在Shopify上创建的“order_created”事件的Webhook,我发现传递给body的请求ID与我从测试数据发送的ID不同,这实际上是body-parser在express中无法很好地处理大整数的问题。

最终,我部署了一些Google云函数,req已经有了原始body,我可以使用它,但在我的Node测试环境中,我实现了以下单独的body parser,因为使用相同的body parser两次会用JSON覆盖原始body。

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}));

基于这个答案

后来,我使用json-bigint解析原始正文以便在其他代码中使用,否则某些数字会受到损坏。


我使用了 body-parser.text() 并解决了这个问题 https://github.com/tiendq/shopify-promobar/blob/master/server/webhooks/route.js#L14 - Tien Do

1

// 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');
    }

}


-1

我也遇到了同样的问题。使用 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();
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接