As the official documentation from Slack says
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`));
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;
},
})
);
- for
application/x-www-form-urlencoded
content type
app.use(
express.urlencoded({
extended: true,
verify: (req, _, buf) => {
req.rawBody = buf;
},
})
);
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});
...
...
...
});
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
, thetimestamp
, and thebody
of the request to form abasestring
. Use a colon as the delimiter between the three elements. For example,v0:123456789:command=/weather&text=94070
. The version number right now is alwaysv0
. - With the help of HMAC SHA256 implemented in the NPM package crypto, hash the above
basestring
, using theSlack Signing Secret
as the key. Compare this computed signature to theX-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 };
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;
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 📓:
- Integrate Razorpay Payment Gateway in your React app
- Some Lesser Known But Awesome Online Tools For Web Development
- Node & Express server, but with TypeScript !
Find me around web 🕸:
- 💻 Visit my Website
- 🌟 Check out my Substack
- 😸 Check my Repos on GitHub
- 🦊 Check my Repos on GitLab
- 📦 Check my Packages on NPM
- 🔗 View my Profile on LinkedIn
- 📝 View my Blogs on Dev.to
- 😜 Follow me on Instagram
- 📚 Check out my Goodreads Profile
- 📪 Contact me Here
Top comments (0)