Node.js 如何验证Slack Events API的请求

Node.js 如何验证Slack Events API的请求,node.js,express,http-post,slack-api,Node.js,Express,Http Post,Slack Api,我正在使用该包验证传入的slack请求是否来自slack。这适用于斜杠命令和交互式组件(按钮等)。但是,它不适用于Events API 我注意到POST请求主体对于事件API有不同的格式。没有有效负载。但我不清楚slack给了我什么来验证。代码如下 //This WORKS app.post("/interactiveCommand", async (req, res) => { const legit = validateSlackRequest(process.

我正在使用该包验证传入的slack请求是否来自slack。这适用于斜杠命令和交互式组件(按钮等)。但是,它不适用于Events API

我注意到POST请求主体对于事件API有不同的格式。没有
有效负载
。但我不清楚slack给了我什么来验证。代码如下

//This WORKS
app.post("/interactiveCommand", async (req, res) => {
  const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
  if (!legit) {
    console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
    return res.status(403).send("Unauthorized");
  }
  await interactiveCommand(...);
  return;
});

//This does NOT WORK
app.post("/slackEvents", parser, json, async (req, res) => {
  const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
  if (!legit) {
    console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
    res.status(403).send("Unauthorized");
  } else {
    try {
      switch (req.body.event.type) {
        case "message":
          await handleMessageEvent(...);
          break;
        case "app_home_opened":
          res.status(200).send();
          await updateUserHomePage(...);
          break;
        default:
          res.status(200).send();
          return;
      }
    } catch(e) {
      console.log("Error with event handling! ", e);
    }
  }
});

const crypto = require('crypto')
const querystring = require('querystring')

// Adhering to RFC 3986
// Inspired from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
function fixedEncodeURIComponent (str) {
  return str.replace(/[!'()*~]/g, function (c) {
    return '%' + c.charCodeAt(0).toString(16).toUpperCase()
  })
}

/**
 * Validate incoming Slack request
 *
 * @param {string} slackAppSigningSecret - Slack application signing secret
 * @param {object} httpReq - Express request object
 * @param {boolean} [logging=false] - Enable logging to console
 *
 * @returns {boolean} Result of vlaidation
 */
function validateSlackRequest (slackAppSigningSecret, httpReq, logging) {
  logging = logging || false
  if (typeof logging !== 'boolean') {
    throw new Error('Invalid type for logging. Provided ' + typeof logging + ', expected boolean')
  }
  if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') {
    throw new Error('Invalid slack app signing secret')
  }
  const xSlackRequestTimeStamp = httpReq.get('X-Slack-Request-Timestamp')
  const SlackSignature = httpReq.get('X-Slack-Signature')
  const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1
  if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
    if (logging) { console.log('Missing part in Slack\'s request') }
    return false
  }
  const baseString = 'v0:' + xSlackRequestTimeStamp + ':' + bodyPayload
  const hash = 'v0=' + crypto.createHmac('sha256', slackAppSigningSecret)
    .update(baseString)
    .digest('hex')

  if (logging) {
    console.log('Slack verifcation:\n Request body: ' + bodyPayload + '\n Calculated Hash: ' + hash + '\n Slack-Signature: ' + SlackSignature)
  }
  return (SlackSignature === hash)
}

我使用以下命令将文本有效负载转换为json:

IFS=$'\n'
if [ "$REQUEST_METHOD" = "POST" ]; then
 if [ "$CONTENT_LENGTH" -gt 0 ]; then
  cat - > /tmp/file.txt
 fi
fi
cat /tmp/file.txt | sed -e "s/^payload=//g" | perl -pe 's/\%(\w\w)/chr hex $1/ge' > /tmp/file.json

这就是我如何让它工作的,这是一个小的尝试和错误,我没有作出承诺。基本上,如果我在验证一个事件,而不是一个斜杠命令或一个交互式组件,我会将
type=“Event”
传递给验证函数。唯一的变化是如何从传入请求构造有效负载

export function validateSlackRequest(
  slackAppSigningSecret,
  httpReq,
  logging,
  type = ""
) {
  logging = logging || false;
  if (typeof logging !== "boolean") {
    throw new Error(
      "Invalid type for logging. Provided " +
        typeof logging +
        ", expected boolean"
    );
  }
  if (
    !slackAppSigningSecret ||
    typeof slackAppSigningSecret !== "string" ||
    slackAppSigningSecret === ""
  ) {
    throw new Error("Invalid slack app signing secret");
  }
  const xSlackRequestTimeStamp = httpReq.get("X-Slack-Request-Timestamp");
  const SlackSignature = httpReq.get("X-Slack-Signature");
  let bodyPayload;
  if (type === "Event") {
    bodyPayload = (httpReq as any).rawBody;
  } else {
    bodyPayload = fixedEncodeURIComponent(
      querystring.stringify(httpReq.body).replace(/%20/g, "+")
    ); // Fix for #1
  }
  if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
    if (logging) {
      console.log("Missing part in Slack's request");
    }
    return false;
  }
  const baseString = "v0:" + xSlackRequestTimeStamp + ":" + bodyPayload;
  const hash =
    "v0=" +
    crypto
      .createHmac("sha256", slackAppSigningSecret)
      .update(baseString)
      .digest("hex");

  if (logging) {
    console.log(
      "Slack verification:\nTimestamp: " +
        xSlackRequestTimeStamp +
        "\n Request body: " +
        bodyPayload +
        "\n Calculated Hash: " +
        hash +
        "\n Slack-Signature: " +
        SlackSignature
    );
  }
  return SlackSignature === hash;
}

如果我不想弄乱tmp文件,你能分享更多关于如何在节点环境中工作的信息吗?抱歉,我不能。我的环境是BASH脚本,所以我不能谈论node如何/在何处收集POST负载。但是,如果您可以创建自己的可执行shell脚本,这些脚本可以从节点内调用,并且可以将POST负载输入其中并捕获输出,那么shell脚本本身只需要一行:sed-e“s//^payload=//g”| perl-pe的//\%(\w\w)/chr hex$1/ge'googleling声称节点中存在PERL,因此您真正要做的就是删除POST数据开头的“payload=”,然后将所有HTML实体转换为它们的可视等价物。JSON就在那里——它刚刚被编码用于web查看(出于某种原因)。在我看来,Events API主体有效负载的格式(JSON)与我们从Slack获得的所有其他有效负载的格式相同。你是说事件API发送文本有效载荷?还是与有效载荷的结构有关?块事件的原始POST正文如下所示:
有效载荷=%7B%22type%22%3A%22block_操作%22%2C%22user%22%3A%7B%22id
切断有效载荷=并将这些十六进制代码作为其实际字符,给出了这个
{“type”:“block_操作”,“user”:{“id
如果您已经看到JSON,那么这对您来说不是问题。