DEV Community

Cover image for Verifying requests from Slack - The CORRECT method for Node.js
Soumya Dey
Soumya Dey

Posted on • Edited on

Verifying requests from Slack - The CORRECT method for Node.js

As the official documentation from Slack says

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

There are some blogs and tutorials available for helping you do this verification, but nearly all of them ignore one very important point in the process. And because of this when I had to implement this verification process in a backend server API service I struggled a long time because the signature coming from Slack and the signature calculated by me in the server was not matching. I finally made it work after one advice from the Slack's team.

We need to confirm that you're extracting the raw version of the callback request payload and preserving it in its "raw" form when used in code? This is important because the "raw" form of the request body, including whitespace, is used by Slack's system to calculate the signature.

For more details you can check the official documentation where this very important point is written in such a way that if you are not absolutely attentive to each and every line you'll surely miss it, as I did myself.

Sine all the steps are already available in the official docs here Verifying requests from Slack, I am going through the steps in short.

The main objective of this article is to show you how to get the request body is raw format and then use it to calculate the signature to match it with the provided signature from Slack's side.


The steps go like this:

1. Update your main JavaScript file

the file where you have initialized the express app i.e. the entry point to your server API service



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


Enter fullscreen mode Exit fullscreen mode

Add the following middlewares to your app to get the raw request body after the line const app = express();:

  • for application/json content type


app.use(
  express.json({
    verify: (req, _, buf) => {
      req.rawBody = buf;
    },
  })
);


Enter fullscreen mode Exit fullscreen mode
  • for application/x-www-form-urlencoded content type


app.use(
  express.urlencoded({
    extended: true,
    verify: (req, _, buf) => {
      req.rawBody = buf;
    },
  })
);


Enter fullscreen mode Exit fullscreen mode

Now you can access the raw request body inside any of your API routes or any middlewares in your app like the following:



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


Enter fullscreen mode Exit fullscreen mode

2. Verify the requests coming from Slack

Here's an overview of the process to validate a signed request from Slack:

  • Retrieve the X-Slack-Request-Timestamp header on the HTTP request, and the body of the request.
  • Concatenate the version number, the timestamp, and the body of the request to form a basestring. Use a colon as the delimiter between the three elements. For example, v0:123456789:command=/weather&text=94070. The version number right now is always v0.
  • With the help of HMAC SHA256 implemented in the NPM package crypto, hash the above basestring, using the Slack Signing Secret as the key. Compare this computed signature to the X-Slack-Signature header on the request.

This package crypto is now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.

Here I have written a middleware method that I am using in my API routes for handling Slack request [message actions, data submissions, slash commands, webhook payloads etc.]

The code for the middleware is as follows. The method is written in a file named webhookVerifier.js and later the method is imported in the file needed.



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


Enter fullscreen mode Exit fullscreen mode

3. Add the middleware in your routes

You can import the middleware in your routes file and use it in your API routes for Slack like the following



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;


Enter fullscreen mode Exit fullscreen mode

Note: Your API routes will be different than mine. The above code is just an example

The most important point is to use the raw request body instead of the encoded request body. Because of this small but very important point which was missing from my code, I was breaking my head for a long time. Hope that your head will not get that treatment after this tutorial.


My Recent Blog Posts 📓:

Find me around web 🕸:

Top comments (0)