从Slack验证请求 - Node.js的正确方法
#安全 #node #slack

正如Slack的官方文件所说

Slack signs its requests using a secret that's unique to your app.

有一些博客和教程可帮助您进行此验证,但是几乎所有的博客都忽略了过程中一个非常重要的一点。因此,当我不得不在后端服务器API服务中实现此验证过程时,我挣扎了很长时间,因为来自Slack的签名和服务器中我计算的签名不匹配。我终于在Slack的团队的一项建议下工作了。

我们需要确认您正在提取回调请求有效载荷的原始版本,并在代码中使用时以其“原始”形式保存?这很重要,因为Slack的系统使用了请求主体的“原始”形式,包括Whitespace,用于计算签名。

有关更多详细信息,您可以检查以这样的方式编写这个非常重要的观点的正式文档,如果您对每一行不太注意,您一定会想念它,就像我自己一样。

正式所有步骤都已在此处的官方文档中可用

本文的主要目的是向您展示如何获得请求主体是原始格式,然后使用它来计算签名,以将其与Slack侧面提供的签名匹配。


步骤像这样:

1.更新您的主JavaScript文件

您初始化了Express应用的文件,即服务器API服务的入口点

const express = require('express');
const app = express();
...
...
...
app.listen(8000, () => console.log(`server started`));

app中添加以下中间Wares,以在const app = express();之后获取原始请求主体:

  • 对于application/json内容类型
app.use(
  express.json({
    verify: (req, _, buf) => {
      req.rawBody = buf;
    },
  })
);
  • 对于application/x-www-form-urlencoded内容类型
app.use(
  express.urlencoded({
    extended: true,
    verify: (req, _, buf) => {
      req.rawBody = buf;
    },
  })
);

现在,您可以在应用程序中的任何API路由或任何中间件内访问原始请求主体,如下:

const router = require('express').Router();
...
...
...
router.post('/some-route', (req, res) => {
  console.log({rawBody: req.rawBody});
  ...
  ...
  ...
});

2.验证来自Slack的请求

这是从Slack验证签名请求的过程的概述:

  • 在HTTP请求和请求的正文上检索X-Slack-Request-Timestamp标头。
  • 串联version numbertimestampbody,要求形成basestring。使用结肠作为三个元素之间的定界符。例如,v0:123456789:command=/weather&text=94070。现在的版本编号始终是v0
  • NPM package cryptoNPM package crypto中实现的HMAC SHA256的帮助下,将上述basestring hash用作密钥。 在请求中比较此计算的签名与X-Slack-Signature标头。

此软件包加密现在是一个内置节点模块。如果您依靠加密货币,则应切换到内置的。

在这里,我编写了一种中间件方法,该方法在API路由中使用用于处理Slack请求的[消息操作,数据提交,斜杠命令,Webhook有效载荷等]

中间件的代码如下。该方法写在名为webhookVerifier.js的文件中,后来该方法在所需的文件中导入。

const crypto = require('crypto');
...
...
...
const slack = (req, res, next) => {
  // verify that the timestamp does not differ from local time by more than five minutes
  if (
    !req.headers['x-slack-request-timestamp'] ||
    Math.abs(
      Math.floor(new Date().getTime() / 1000) -
        +req.headers['x-slack-request-timestamp']
    ) > 300
  )
    return res.status(400).send('Request too old!');

  // compute the basestring
  const baseStr = `v0:${req.headers['x-slack-request-timestamp']}:${req.rawBody}`;

  // extract the received signature from the request headers
  const receivedSignature = req.headers['x-slack-signature'];

  // compute the signature using the basestring 
  // and hashing it using the signing secret 
  // which can be stored as a environment variable
  const expectedSignature = `v0=${crypto
    .createHmac('sha256', process.env.SLACK_SIGNING_SECRET)
    .update(baseStr, 'utf8')
    .digest('hex')}`;

  // match the two signatures
  if (expectedSignature !== receivedSignature) {
    console.log('WEBHOOK SIGNATURE MISMATCH');
    return res.status(400).send('Error: Signature mismatch security error');
  }

  // signatures matched, so continue the next step
  console.log('WEBHOOK VERIFIED');
  next();
};

// exporting the method
module.exports = { slack };

3.在路线中添加中间件

您可以在路由文件中导入中间件,并在API路由中使用它作为Slack,如以下

const router = require('express').Router();

// importing the webhook verifier method for slack
const webhookVerifier = require('./path/to/the/webhookVerifier/file/webhookVerifier');

// for managing slack user interactions
router.post('/interact', [webhookVerifier.slack], async (req, res) => {
...
...
...
});

// for handling slack slash commands
router.post('/slash', [webhookVerifier.slack], async (req, res) => {
...
...
...
});

// for managing the slack webhook evevt subscription payloads
router.post('/webhook', [webhookVerifier.slack], async (req, res) => {
...
...
...
});

module.exports = router;

注意:您的API路线与我的路线不同。以上代码只是一个示例

最重要的一点是使用原始请求主体而不是编码的请求主体。因为我的代码中缺少这个小但非常重要的一点,所以我很长一段时间以来一直打断头。希望您的头在本教程后不会得到治疗。


我最近的博客文章ð:

在网上找到我: