Last active
September 18, 2020 06:38
-
-
Save seratch/98f4ab384b187ece90086197e24dfbd2 to your computer and use it in GitHub Desktop.
Passing request headers to Bolt JS
This file contains 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
const { LogLevel } = require("@slack/logger"); | |
const { App } = require("@slack/bolt"); | |
const { MyExpressReceiver } = require("./receiver") | |
const app = new App({ | |
token: process.env.SLACK_BOT_TOKEN, | |
logLevel: process.env.SLACK_LOG_LEVEL || LogLevel.DEBUG, | |
receiver: new MyExpressReceiver({ | |
signingSecret: process.env.SLACK_SIGNING_SECRET | |
}), | |
}); | |
async function moveAdditionalPropsToContext(args) { | |
// additionalProperties is a workaround as of @slack/bolt 2.3.0 | |
Object.assign(args.context, args.body.additionalProperties); | |
delete args.body.additionalProperties; | |
return await args.next(); | |
} | |
async function printMiddlewareArgs(args) { | |
const copiedArgs = JSON.parse(JSON.stringify(args)); | |
copiedArgs.context.botToken = 'xoxb-***'; | |
if (copiedArgs.context.userToken) { | |
copiedArgs.context.userToken = 'xoxp-***'; | |
} | |
copiedArgs.client = {}; | |
copiedArgs.logger = {}; | |
args.logger.debug( | |
"Dumping request data for debugging...\n\n" + | |
JSON.stringify(copiedArgs, null, 2) + | |
"\n" | |
); | |
const result = await args.next(); | |
args.logger.debug("next() call completed"); | |
return result; | |
} | |
app.use(moveAdditionalPropsToContext); | |
app.use(printMiddlewareArgs); | |
app.event("app_mention", async ({ logger, event, say }) => { | |
await say({ text: `:wave: <@${event.user}> Hi there!` }); | |
}); | |
(async () => { | |
await app.start(process.env.PORT || 3000); | |
console.log("⚡️ Bolt app is running!"); | |
})(); | |
// [DEBUG] bolt-app Dumping request data for debugging... | |
// { | |
// "body": { | |
// "token": "verification-token", | |
// "team_id": "T111", | |
// "enterprise_id": "E013Y3SHLAY", | |
// "api_app_id": "A0160BH7X62", | |
// "event": { | |
// "client_msg_id": "11ea3aad-1d8e-4b7e-a7d4-b71dcc6370a5", | |
// "type": "app_mention", | |
// "text": "<@W111>", | |
// "user": "W111", | |
// "ts": "1600410693.000300", | |
// "team": "T111", | |
// "blocks": [ | |
// { | |
// "type": "rich_text", | |
// "block_id": "CF6y", | |
// "elements": [ | |
// { | |
// "type": "rich_text_section", | |
// "elements": [ | |
// { | |
// "type": "user", | |
// "user_id": "W111" | |
// } | |
// ] | |
// } | |
// ] | |
// } | |
// ], | |
// "channel": "C111", | |
// "event_ts": "1600410693.000300" | |
// }, | |
// "type": "event_callback", | |
// "event_id": "Ev111", | |
// "event_time": 1600410693, | |
// "authed_users": [ | |
// "W111" | |
// ] | |
// }, | |
// "context": { | |
// "botToken": "xoxb-***", | |
// "botUserId": "W111", | |
// "botId": "B111", | |
// "http": { | |
// "headers": { | |
// "host": "www.example.com", | |
// "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)", | |
// "accept": "*/*", | |
// "accept-encoding": "gzip,deflate", | |
// "content-type": "application/json", | |
// "x-slack-signature": "v0=xsdfdsfdsfdsfdsfdsfds", | |
// "x-slack-request-timestamp": "1600410694", | |
// "content-length": "599", | |
// "x-forwarded-proto": "https", | |
// "x-forwarded-for": "54.146.233.202" | |
// } | |
// } | |
// }, | |
// "client": {}, | |
// "logger": {} | |
// } |
This file contains 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
{ | |
"name": "bolt-starter", | |
"version": "0.0.1", | |
"description": "Bolt app starter", | |
"main": "index.js", | |
"scripts": { | |
"local": "node_modules/.bin/nodemon index.js", | |
"start": "node index.js" | |
}, | |
"repository": { | |
"type": "git", | |
"url": "git@github.com:seratch/bolt-starter.git" | |
}, | |
"keywords": [ | |
"Slack", | |
"Bolt" | |
], | |
"author": "@seratch", | |
"license": "MIT", | |
"bugs": { | |
"url": "https://github.com/seratch/bolt-starter/issues" | |
}, | |
"homepage": "https://github.com/seratch/bolt-starter#readme", | |
"dependencies": { | |
"@slack/bolt": "^2.3.0", | |
"dotenv": "^8.2.0" | |
}, | |
"devDependencies": { | |
"nodemon": "^2.0.4" | |
} | |
} |
This file contains 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
const { createServer } = require('http'); | |
const express = require('express'); | |
const rawBody = require('raw-body'); | |
const querystring = require('querystring'); | |
const crypto = require('crypto'); | |
const tsscmp = require('tsscmp'); | |
const { ConsoleLogger } = require('@slack/logger'); | |
const { ReceiverAuthenticityError, ReceiverMultipleAckError } = require('@slack/bolt/dist/errors'); | |
class MyExpressReceiver { | |
constructor({ | |
signingSecret = '', | |
logger = new ConsoleLogger(), | |
endpoints = { events: '/slack/events' }, | |
}) { | |
this.app = express(); | |
this.server = createServer(this.app); | |
const expressMiddleware = [ | |
verifySignatureAndParseRawBody(logger, signingSecret), | |
respondToSslCheck, | |
respondToUrlVerification, | |
this.requestHandler.bind(this), | |
]; | |
this.logger = logger; | |
const endpointList = typeof endpoints === 'string' ? [endpoints] : Object.values(endpoints); | |
this.router = express.Router(); | |
endpointList.forEach((endpoint) => { | |
this.router.post(endpoint, ...expressMiddleware); | |
}); | |
this.app.use(this.router); | |
} | |
async requestHandler(req, res) { | |
var _a; | |
let isAcknowledged = false; | |
setTimeout(() => { | |
if (!isAcknowledged) { | |
this.logger.error('An incoming event was not acknowledged within 3 seconds. ' + | |
'Ensure that the ack() argument is called in a listener.'); | |
} | |
}, 3001); | |
let storedResponse; | |
// As of @slack/bolt v2.3.0, adding a temporary field to body is the only way to tell additional data to App | |
req.body["additionalProperties"] = { | |
http: { | |
headers: req.headers | |
} | |
} | |
const event = { | |
body: req.body, | |
// Ideally, we would like to go with this way, but it doesn't work as of v2.3.0 | |
// context: { | |
// http: { | |
// headers: req.headers | |
// }, | |
// }, | |
ack: async (response) => { | |
if (isAcknowledged) { | |
throw new ReceiverMultipleAckError(); | |
} | |
isAcknowledged = true; | |
if (!response) { | |
res.send(''); | |
} | |
else if (typeof response === 'string') { | |
res.send(response); | |
} | |
else { | |
res.json(response); | |
} | |
}, | |
}; | |
try { | |
await ((_a = this.bolt) === null || _a === void 0 ? void 0 : _a.processEvent(event)); | |
if (storedResponse !== undefined) { | |
if (typeof storedResponse === 'string') { | |
res.send(storedResponse); | |
} | |
else { | |
res.json(storedResponse); | |
} | |
} | |
} | |
catch (err) { | |
res.status(500).send(); | |
throw err; | |
} | |
} | |
init(bolt) { | |
this.bolt = bolt; | |
} | |
start(port) { | |
return new Promise((resolve, reject) => { | |
try { | |
this.server.listen(port, () => { | |
resolve(this.server); | |
}); | |
} | |
catch (error) { | |
reject(error); | |
} | |
}); | |
} | |
stop() { | |
return new Promise((resolve, reject) => { | |
this.server.close((error) => { | |
if (error !== undefined) { | |
reject(error); | |
return; | |
} | |
resolve(); | |
}); | |
}); | |
} | |
} | |
const respondToSslCheck = (req, res, next) => { | |
if (req.body && req.body.ssl_check) { | |
res.send(); | |
return; | |
} | |
next(); | |
}; | |
const respondToUrlVerification = (req, res, next) => { | |
if (req.body && req.body.type && req.body.type === 'url_verification') { | |
res.json({ challenge: req.body.challenge }); | |
return; | |
} | |
next(); | |
}; | |
function verifySignatureAndParseRawBody(logger, signingSecret) { | |
return async (req, res, next) => { | |
let stringBody; | |
const preparsedRawBody = req.rawBody; | |
if (preparsedRawBody !== undefined) { | |
stringBody = preparsedRawBody.toString(); | |
} | |
else { | |
stringBody = (await rawBody(req)).toString(); | |
} | |
try { | |
req.body = verifySignatureAndParseBody(signingSecret, stringBody, req.headers); | |
} | |
catch (error) { | |
if (error) { | |
if (error instanceof ReceiverAuthenticityError) { | |
logError(logger, 'Request verification failed', error); | |
return res.status(401).send(); | |
} | |
logError(logger, 'Parsing request body failed', error); | |
return res.status(400).send(); | |
} | |
} | |
return next(); | |
}; | |
} | |
function logError(logger, message, error) { | |
const logMessage = 'code' in error | |
? `${message} (code: ${error.code}, message: ${error.message})` | |
: `${message} (error: ${error})`; | |
logger.warn(logMessage); | |
} | |
function verifyRequestSignature(signingSecret, body, signature, requestTimestamp) { | |
if (signature === undefined || requestTimestamp === undefined) { | |
throw new ReceiverAuthenticityError('Slack request signing verification failed. Some headers are missing.'); | |
} | |
const ts = Number(requestTimestamp); | |
if (isNaN(ts)) { | |
throw new ReceiverAuthenticityError('Slack request signing verification failed. Timestamp is invalid.'); | |
} | |
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5; | |
if (ts < fiveMinutesAgo) { | |
throw new ReceiverAuthenticityError('Slack request signing verification failed. Timestamp is too old.'); | |
} | |
const hmac = crypto.createHmac('sha256', signingSecret); | |
const [version, hash] = signature.split('='); | |
hmac.update(`${version}:${ts}:${body}`); | |
if (!tsscmp(hash, hmac.digest('hex'))) { | |
throw new ReceiverAuthenticityError('Slack request signing verification failed. Signature mismatch.'); | |
} | |
} | |
function verifySignatureAndParseBody(signingSecret, body, headers) { | |
const { | |
'x-slack-signature': signature, | |
'x-slack-request-timestamp': requestTimestamp, | |
'content-type': contentType, | |
} = headers; | |
verifyRequestSignature(signingSecret, body, signature, requestTimestamp); | |
return parseRequestBody(body, contentType); | |
} | |
function parseRequestBody(stringBody, contentType) { | |
if (contentType === 'application/x-www-form-urlencoded') { | |
const parsedBody = querystring.parse(stringBody); | |
if (typeof parsedBody.payload === 'string') { | |
return JSON.parse(parsedBody.payload); | |
} | |
return parsedBody; | |
} | |
return JSON.parse(stringBody); | |
} | |
exports.MyExpressReceiver = MyExpressReceiver; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment