verify-slack-request.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. import { createHmac, timingSafeEqual } from 'crypto';
  2. import { Response, NextFunction } from 'express';
  3. import createError from 'http-errors';
  4. import { stringify } from 'qs';
  5. import { RequestFromSlack } from '../interfaces/request-from-slack';
  6. import loggerFactory from '../utils/logger';
  7. const logger = loggerFactory('@growi/slack:middlewares:verify-slack-request');
  8. /**
  9. * Verify if the request came from slack
  10. * See: https://api.slack.com/authentication/verifying-requests-from-slack
  11. */
  12. export const verifySlackRequest = (req: RequestFromSlack & { rawBody: any }, res: Response, next: NextFunction): Record<string, any> | void => {
  13. const signingSecret = req.slackSigningSecret;
  14. if (signingSecret == null) {
  15. const message = 'No signing secret.';
  16. logger.warn(message, { body: req.body });
  17. return next(createError(400, message));
  18. }
  19. // take out slackSignature and timestamp from header
  20. const slackSignature = req.headers['x-slack-signature'];
  21. const timestamp = req.headers['x-slack-request-timestamp'];
  22. if (slackSignature == null || timestamp == null) {
  23. const message = 'Forbidden. Enter from Slack workspace';
  24. logger.warn(message, { body: req.body });
  25. return next(createError(403, message));
  26. }
  27. // protect against replay attacks
  28. const time = Math.floor(new Date().getTime() / 1000);
  29. if (Math.abs(time - timestamp) > 300) {
  30. const message = 'Verification failed.';
  31. logger.warn(message, { body: req.body });
  32. return next(createError(403, message));
  33. }
  34. // use req.rawBody for Events API
  35. // reference: https://stackoverflow.com/questions/64794287/how-to-verify-a-request-from-slack-events-api
  36. let sigBaseString: string;
  37. if (req.body.event != null) {
  38. sigBaseString = `v0:${timestamp}:${req.rawBody}`;
  39. }
  40. else {
  41. sigBaseString = `v0:${timestamp}:${stringify(req.body, { format: 'RFC1738' })}`;
  42. }
  43. // generate growi signature
  44. const hasher = createHmac('sha256', signingSecret);
  45. hasher.update(sigBaseString, 'utf8');
  46. const hashedSigningSecret = hasher.digest('hex');
  47. const growiSignature = `v0=${hashedSigningSecret}`;
  48. // compare growiSignature and slackSignature
  49. if (timingSafeEqual(Buffer.from(growiSignature, 'utf8'), Buffer.from(slackSignature, 'utf8'))) {
  50. return next();
  51. }
  52. const message = 'Verification failed.';
  53. logger.warn(message, { body: req.body });
  54. return next(createError(403, message));
  55. };