4
votes

I am using the validate-slack-request package to validate my incoming slack requests are from slack. This works fine for Slash commands and Interactive Components (buttons, etc.). However it is not working for the Events API

I noticed that the POST request body has a different format for the Events API. There's no payload. But it's not clear to me what slack is giving me to use to verify. Code is below

//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)
}
2

2 Answers

0
votes

I convert text payloads into json with this:

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
0
votes

This is how I got it to work, it was a little bit of trial and error and I make no promises. Basically, if I was validating an Event rather than a Slash Command or an Interactive Component, I would pass type="Event" to the validation function. The only change is how I construct the payload from the incoming request

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