Last active
December 11, 2024 07:08
-
-
Save alasano/c66f6e5c03518306ba94cf2ea4617bfc to your computer and use it in GitHub Desktop.
NestJS Slack Signature Verification - Drop this guard into any controller which needs to verify the authenticity of requests coming from Slack! @UseGuards(SlackGuard) - Based on https://api.slack.com/docs/verifying-requests-from-slack
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; | |
| import * as qs from 'qs'; | |
| import * as crypto from 'crypto'; | |
| import { Buffer } from 'buffer'; | |
| @Injectable() | |
| export class SlackGuard implements CanActivate { | |
| canActivate( | |
| context: ExecutionContext, | |
| ): boolean { | |
| const { headers, body } = context.switchToHttp().getRequest(); | |
| const hmac = crypto.createHmac('sha256', process.env.SLACK_SIGNING_SECRET) | |
| const rawBody = qs.stringify(body).replace(/%20/g, "+"); | |
| const slackSignature = headers['x-slack-signature']; | |
| const requestTimestamp = headers['x-slack-request-timestamp']; | |
| // ~~ is equivalent to Math.floor() | |
| const timeInSeconds = ~~(new Date().getTime() / 1000); | |
| // Reject if request is older than 5 minutes | |
| if (Math.abs(timeInSeconds - requestTimestamp) > 300) return false; | |
| const signatureBaseString = `v0:${requestTimestamp}:${rawBody}`; | |
| const mySignature = 'v0=' + hmac.update(signatureBaseString, 'utf8').digest('hex'); | |
| return crypto.timingSafeEqual( | |
| Buffer.from(mySignature, 'utf8'), | |
| Buffer.from(slackSignature, 'utf8') | |
| ); | |
| } | |
| } |
Author
Hey @El-Fitz!
That's an interesting discovery! I've tested it and it seems to be good, it's only about replacing spaces with "+" after all.
Glad to see that people are stumbling upon this bit of code otherwise. Merci!
Hi @alasano!
You're welcome! Your gist helped me tremendously ^^
For me, const rawBody = qs.stringify(body).replace(/%20/g, "+"); didn't work, because I am receiving JSON-formatted requests from Slack, and thus need to use the request.rawBody as received (in JSON) to calculate the signature.
I did this instead:
const request = context.switchToHttp().getRequest<RawBodyRequest<Request>>();
// ...
const rawBody = request.rawBody?.toString('utf8') as string;You are my savier!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi!
Just wanted to let you know, after getting through a bit of trouble myself, that
qs.stringify(body, { format: 'RFC1738' }), although it properly decodes the body, will make the signature verification fail if the body contains%28(() or%29()). Seems likeqs.stringify(body).replace(/%20/g, "+")solves it and otherwise works just as well.