From 9e94a450fd99bde8380553d2494404cbca176252 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Fri, 5 Jun 2020 23:09:05 -0400 Subject: [PATCH 01/19] support for http.ClientRequest --- index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 266fae7..194592f 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ function fixedEncodeURIComponent (str) { * Validate incoming Slack request * * @param {string} slackAppSigningSecret - Slack application signing secret - * @param {object} httpReq - Express request object + * @param {object} httpReq - http.ClientRequest or Express request object * @param {boolean} [logging=false] - Enable logging to console * * @returns {boolean} Result of vlaidation @@ -26,8 +26,9 @@ function validateSlackRequest (slackAppSigningSecret, httpReq, logging) { if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') { throw new Error('Invalid slack app signing secret') } - const xSlackRequestTimeStamp = httpReq.get('X-Slack-Request-Timestamp') - const SlackSignature = httpReq.get('X-Slack-Signature') + const get = httpReq.getHeader || httpReq.get // Fix for #5 + const xSlackRequestTimeStamp = get('X-Slack-Request-Timestamp') + const SlackSignature = get('X-Slack-Signature') const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1 if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) { if (logging) { console.log('Missing part in Slack\'s request') } From 8e879c49dbd60e6dcd7fd09ba60de5d4147bb9c7 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Fri, 5 Jun 2020 23:33:45 -0400 Subject: [PATCH 02/19] bind the HTTP request object "get" method to the object --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 194592f..90d475f 100644 --- a/index.js +++ b/index.js @@ -26,7 +26,7 @@ function validateSlackRequest (slackAppSigningSecret, httpReq, logging) { if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') { throw new Error('Invalid slack app signing secret') } - const get = httpReq.getHeader || httpReq.get // Fix for #5 + const get = (httpReq.getHeader || httpReq.get).bind(httpReq) // Fix for #5 const xSlackRequestTimeStamp = get('X-Slack-Request-Timestamp') const SlackSignature = get('X-Slack-Signature') const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1 From 9781702f10201a3182ab69a65ad066ba909646bc Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Fri, 5 Jun 2020 23:33:59 -0400 Subject: [PATCH 03/19] add tests for http.ClientRequest "getHeader" support --- test/validate-slack-request.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/validate-slack-request.js b/test/validate-slack-request.js index a663093..14d932b 100644 --- a/test/validate-slack-request.js +++ b/test/validate-slack-request.js @@ -6,8 +6,8 @@ var assert = require('assert') // Test object. Simulate an express request object var slackSigningSecret = '8f742231b10e8888abcd99yyyzzz85a5' -function getTestHttpRequest (textArgs) { - return { +function getTestHttpRequest (textArgs, get='get') { + var req = { 'X-Slack-Request-Timestamp': '1531420618', 'body': { 'token': 'xyzz0WbapA4vBCDEFasx0q6G', @@ -23,10 +23,11 @@ function getTestHttpRequest (textArgs) { 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c' }, 'X-Slack-Signature': textArgs ? 'v0=a3e650d30d1e91901834f91d048c9d3c0a50e4dcffcef7bc67884e95df8588ce' : 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', - get: function (element) { - return this[element] - } } + req[get] = function (element) { + return this[element] + } + return req } var testHttpRequest @@ -113,4 +114,10 @@ describe('Slack incoming request test', function () { assert.throws(() => { slackValidateRequest(slackSigningSecret, testHttpRequest, 1) }) }) }) + + describe('Using a http.ClientRequest for the request object', function() { + it('function normally', function() { + assert.equal(slackValidateRequest(slackSigningSecret, getTestHttpRequest(undefined, 'getHeader')), true) + }) + }) }) From c15f50e1e5c472ddce7160711208a895d5b87fbb Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Fri, 5 Jun 2020 23:34:35 -0400 Subject: [PATCH 04/19] fix typo for http.ClientRequest support tess --- test/validate-slack-request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/validate-slack-request.js b/test/validate-slack-request.js index 14d932b..83c4895 100644 --- a/test/validate-slack-request.js +++ b/test/validate-slack-request.js @@ -116,7 +116,7 @@ describe('Slack incoming request test', function () { }) describe('Using a http.ClientRequest for the request object', function() { - it('function normally', function() { + it('should function normally', function() { assert.equal(slackValidateRequest(slackSigningSecret, getTestHttpRequest(undefined, 'getHeader')), true) }) }) From d2e9b9b90291fc329cb5f869589d7138769c06a6 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Fri, 5 Jun 2020 23:45:52 -0400 Subject: [PATCH 05/19] version bump for http.ClientRequest support --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cfa837..b6dcafe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "validate-slack-request", - "version": "0.1.4", + "version": "0.1.5", "description": "A simple module to validate slack request in Express", "main": "index.js", "scripts": { From 58d6927684a4b41f2e38f9cee32b22e0abe107ee Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Fri, 5 Jun 2020 23:47:04 -0400 Subject: [PATCH 06/19] update package.json to reflect http.ClientRequest support --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b6dcafe..5b8f0d4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "validate-slack-request", "version": "0.1.5", - "description": "A simple module to validate slack request in Express", + "description": "A simple module to validate Slack requests", "main": "index.js", "scripts": { "test": "mocha" @@ -12,6 +12,7 @@ }, "keywords": [ "slack", + "http", "express" ], "author": "gverni", From 4e75ddcb4bf9ce4438236b2f97b182d009187df5 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Sat, 6 Jun 2020 00:37:48 -0400 Subject: [PATCH 07/19] resolve incompatibility with http.IncomingMessage --- index.js | 9 +++++++-- test/validate-slack-request.js | 28 ++++++++++++++++------------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 90d475f..c604dfd 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ function fixedEncodeURIComponent (str) { * Validate incoming Slack request * * @param {string} slackAppSigningSecret - Slack application signing secret - * @param {object} httpReq - http.ClientRequest or Express request object + * @param {object} httpReq - http.IncomingMessage or Express request object * @param {boolean} [logging=false] - Enable logging to console * * @returns {boolean} Result of vlaidation @@ -26,7 +26,12 @@ function validateSlackRequest (slackAppSigningSecret, httpReq, logging) { if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') { throw new Error('Invalid slack app signing secret') } - const get = (httpReq.getHeader || httpReq.get).bind(httpReq) // Fix for #5 + const get = ( + httpReq.get + ||(function(str) { + return this.headers[str.toLowerCase()] + })).bind(httpReq); // Fix for #5 + const xSlackRequestTimeStamp = get('X-Slack-Request-Timestamp') const SlackSignature = get('X-Slack-Signature') const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1 diff --git a/test/validate-slack-request.js b/test/validate-slack-request.js index 83c4895..80bcc7a 100644 --- a/test/validate-slack-request.js +++ b/test/validate-slack-request.js @@ -6,9 +6,12 @@ var assert = require('assert') // Test object. Simulate an express request object var slackSigningSecret = '8f742231b10e8888abcd99yyyzzz85a5' -function getTestHttpRequest (textArgs, get='get') { +function getTestHttpRequest (textArgs, useGet=true) { var req = { - 'X-Slack-Request-Timestamp': '1531420618', + 'headers': { + 'x-slack-request-timestamp': '1531420618', + 'x-slack-signature': textArgs ? 'v0=a3e650d30d1e91901834f91d048c9d3c0a50e4dcffcef7bc67884e95df8588ce' : 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', + }, 'body': { 'token': 'xyzz0WbapA4vBCDEFasx0q6G', 'team_id': 'T1DC2JH3J', @@ -22,10 +25,11 @@ function getTestHttpRequest (textArgs, get='get') { 'response_url': 'https://hooks.slack.com/commands/T1DC2JH3J/397700885554/96rGlfmibIGlgcZRskXaIFfN', 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c' }, - 'X-Slack-Signature': textArgs ? 'v0=a3e650d30d1e91901834f91d048c9d3c0a50e4dcffcef7bc67884e95df8588ce' : 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', } - req[get] = function (element) { - return this[element] + if (useGet) { + req.get = function (element) { + return req['headers'][element.toLowerCase()] + } } return req } @@ -48,7 +52,7 @@ describe('Slack incoming request test', function () { describe('Test special characters in command', function() { it('should return true', function() { testHttpRequest = getTestHttpRequest('(!)') - testHttpRequest['X-Slack-Signature'] = 'v0=85b7bd32a59380ae4a50db6d76eed906f36daec1660ceced4907f44eaaf60757' + testHttpRequest.headers['x-slack-signature'] = 'v0=85b7bd32a59380ae4a50db6d76eed906f36daec1660ceced4907f44eaaf60757' assert.equal(slackValidateRequest('slackSigningSecret', testHttpRequest), true) }) }) @@ -56,7 +60,7 @@ describe('Slack incoming request test', function () { describe('Wrong signature', function () { it('should return false if the signature doesn\'t match', function () { testHttpRequest = getTestHttpRequest() - testHttpRequest['X-Slack-Signature'] = 'v0=a2114d57b58eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503' + testHttpRequest.headers['x-slack-signature'] = 'v0=a2114d57b58eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503' assert.equal(slackValidateRequest(slackSigningSecret, testHttpRequest), false) }) }) @@ -71,7 +75,7 @@ describe('Slack incoming request test', function () { describe('Wrong Timestamp', function () { it('should return false if the timestamp is wrong', function () { testHttpRequest = getTestHttpRequest() - testHttpRequest['X-Slack-Request-Timestamp'] = '1531420619' + testHttpRequest.headers['x-slack-request-timestamp'] = '1531420619' assert.equal(slackValidateRequest(slackSigningSecret, testHttpRequest), false) }) }) @@ -87,8 +91,8 @@ describe('Slack incoming request test', function () { describe('Using an invalid slack request', function () { it('should return false', function () { testHttpRequest = getTestHttpRequest() - delete testHttpRequest['X-Slack-Request-Timestamp'] - delete testHttpRequest['X-Slack-Signature'] + delete testHttpRequest.headers['x-slack-request-timestamp'] + delete testHttpRequest.headers['x-slack-signature'] assert.equal(slackValidateRequest(slackSigningSecret, testHttpRequest), false) }) }) @@ -115,9 +119,9 @@ describe('Slack incoming request test', function () { }) }) - describe('Using a http.ClientRequest for the request object', function() { + describe('Using a http.IncomingMessage for the request object', function() { it('should function normally', function() { - assert.equal(slackValidateRequest(slackSigningSecret, getTestHttpRequest(undefined, 'getHeader')), true) + assert.equal(slackValidateRequest(slackSigningSecret, getTestHttpRequest(undefined, false)), true) }) }) }) From 4e098960ffafef5e0ae7bf434d04bd078e6509d5 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 14:54:21 -0400 Subject: [PATCH 08/19] feat: add support for both Next.js and http.IncomingMessage --- index.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index c604dfd..6103de9 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,20 @@ function fixedEncodeURIComponent (str) { }) } +// Reads the body payload from an http.IncomingMessage stream +function getBodyFromStream(httpReq) { + return new Promise((resolve, reject) => { + let bodyString = '' + httpReq.on('data', chunk => bodyString += chunk.toString()); + httpReq.on('end', () => { + resolve(bodyString) + }) + http.on('error', err => { + reject(err) + }) + }) +} + /** * Validate incoming Slack request * @@ -16,9 +30,9 @@ function fixedEncodeURIComponent (str) { * @param {object} httpReq - http.IncomingMessage or Express request object * @param {boolean} [logging=false] - Enable logging to console * - * @returns {boolean} Result of vlaidation + * @returns {boolean} Result of validation */ -function validateSlackRequest (slackAppSigningSecret, httpReq, logging) { +async function validateSlackRequest (slackAppSigningSecret, httpReq, logging) { logging = logging || false if (typeof logging !== 'boolean') { throw new Error('Invalid type for logging. Provided ' + typeof logging + ', expected boolean') @@ -26,15 +40,22 @@ function validateSlackRequest (slackAppSigningSecret, httpReq, logging) { if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') { throw new Error('Invalid slack app signing secret') } + const get = ( httpReq.get ||(function(str) { return this.headers[str.toLowerCase()] - })).bind(httpReq); // Fix for #5 + })).bind(httpReq); // support for http.IncomingRequest headers const xSlackRequestTimeStamp = get('X-Slack-Request-Timestamp') const SlackSignature = get('X-Slack-Signature') - const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1 + let bodyPayload + if (typeof httpReq.body === 'object') { + bodyPayload = querystring.stringify(httpReq.body) + } else { + bodyPayload = await getBodyFromStream(httpReq) // support for http.IncomingRequest stream + } + bodyPayload = fixedEncodeURIComponent(bodyPayload.replace(/%20/g, '+')) // Fix for #1 if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) { if (logging) { console.log('Missing part in Slack\'s request') } return false From 200ccda8e8191f0926e46a7bc837cbbee0cf2435 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 14:55:07 -0400 Subject: [PATCH 09/19] test: run separate tests for Next.js and Express support --- test/express.test.js | 32 +++++++++ test/next.test.js | 29 ++++++++ test/util.js | 102 ++++++++++++++++++++++++++ test/validate-slack-request.js | 127 --------------------------------- 4 files changed, 163 insertions(+), 127 deletions(-) create mode 100644 test/express.test.js create mode 100644 test/next.test.js create mode 100644 test/util.js delete mode 100644 test/validate-slack-request.js diff --git a/test/express.test.js b/test/express.test.js new file mode 100644 index 0000000..8065fd5 --- /dev/null +++ b/test/express.test.js @@ -0,0 +1,32 @@ +/** + * Tests for the Express request object + */ + +const runTests = require('./util') + +function getTestHttpRequest (textArgs) { + return { + 'headers': { + 'x-slack-request-timestamp': '1531420618', + 'x-slack-signature': textArgs ? 'v0=a3e650d30d1e91901834f91d048c9d3c0a50e4dcffcef7bc67884e95df8588ce' : 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', + }, + 'body': { + 'token': 'xyzz0WbapA4vBCDEFasx0q6G', + 'team_id': 'T1DC2JH3J', + 'team_domain': 'testteamnow', + 'channel_id': 'G8PSS9T3V', + 'channel_name': 'foobar', + 'user_id': 'U2CERLKJA', + 'user_name': 'roadrunner', + 'command': '/webhook-collect', + 'text': textArgs || '', + 'response_url': 'https://hooks.slack.com/commands/T1DC2JH3J/397700885554/96rGlfmibIGlgcZRskXaIFfN', + 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c' + }, + get: function (element) { + return this.headers[element.toLowerCase()] + } + } +} + +runTests('Express', getTestHttpRequest) diff --git a/test/next.test.js b/test/next.test.js new file mode 100644 index 0000000..fa02b29 --- /dev/null +++ b/test/next.test.js @@ -0,0 +1,29 @@ +/** + * Tests for the Next.js request object + */ + +const runTests = require('./util') + +function getTestHttpRequest (textArgs) { + return { + 'headers': { + 'x-slack-request-timestamp': '1531420618', + 'x-slack-signature': textArgs ? 'v0=a3e650d30d1e91901834f91d048c9d3c0a50e4dcffcef7bc67884e95df8588ce' : 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', + }, + 'body': { + 'token': 'xyzz0WbapA4vBCDEFasx0q6G', + 'team_id': 'T1DC2JH3J', + 'team_domain': 'testteamnow', + 'channel_id': 'G8PSS9T3V', + 'channel_name': 'foobar', + 'user_id': 'U2CERLKJA', + 'user_name': 'roadrunner', + 'command': '/webhook-collect', + 'text': textArgs || '', + 'response_url': 'https://hooks.slack.com/commands/T1DC2JH3J/397700885554/96rGlfmibIGlgcZRskXaIFfN', + 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c' + }, + } +} + +runTests('Next.js', getTestHttpRequest) \ No newline at end of file diff --git a/test/util.js b/test/util.js new file mode 100644 index 0000000..817d099 --- /dev/null +++ b/test/util.js @@ -0,0 +1,102 @@ +/* eslint-env mocha */ +const assert = require('assert') +const slackValidateRequest = require('..') + + +// Test object. Simulate an express request object +const slackSigningSecret = '8f742231b10e8888abcd99yyyzzz85a5' +let testHttpRequest + +/** + * Runs a set of tests against an HTTP request + * object generated by the given framework + * @param {string} frameworkName + * @param {function} getTestHttpRequest + */ +function runTests(frameworkName, getTestHttpRequest) { + describe(`${frameworkName} request test`, function () { + describe('Basic test', async function () { + it('should return true with test object', async function () { + assert.equal(await slackValidateRequest(slackSigningSecret, getTestHttpRequest()), true) + }) + }) + + describe('Test multiple args', function () { + it('should return true', async function () { + assert.equal(await slackValidateRequest(slackSigningSecret, getTestHttpRequest('args1 args2')), true) + }) + }) + + describe('Test special characters in command', function() { + it('should return true', async function() { + testHttpRequest = getTestHttpRequest('(!)') + testHttpRequest.headers['x-slack-signature'] = 'v0=85b7bd32a59380ae4a50db6d76eed906f36daec1660ceced4907f44eaaf60757' + assert.equal(await slackValidateRequest('slackSigningSecret', testHttpRequest), true) + }) + }) + + describe('Wrong signature', function () { + it('should return false if the signature doesn\'t match', async function () { + testHttpRequest = getTestHttpRequest() + testHttpRequest.headers['x-slack-signature'] = 'v0=a2114d57b58eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503' + assert.equal(await slackValidateRequest(slackSigningSecret, testHttpRequest), false) + }) + }) + + describe('Wrong Signing Secret', function () { + it('should return false if the signing secret is not the correct one', async function () { + var tmpSlackSigningSecret = '9f742231b10e8888abcd99yyyzzz85a5' + assert.equal(await slackValidateRequest(tmpSlackSigningSecret, getTestHttpRequest()), false) + }) + }) + + describe('Wrong Timestamp', function () { + it('should return false if the timestamp is wrong', async function () { + testHttpRequest = getTestHttpRequest() + testHttpRequest.headers['x-slack-request-timestamp'] = '1531420619' + assert.equal(await slackValidateRequest(slackSigningSecret, testHttpRequest), false) + }) + }) + + describe('Wrong body', function () { + it('should return false if the body is not the correct one', async function () { + testHttpRequest = getTestHttpRequest() + testHttpRequest.body.text = 'test' + assert.equal(await slackValidateRequest(slackSigningSecret, testHttpRequest), false) + }) + }) + + describe('Using an invalid slack request', function () { + it('should return false', async function () { + testHttpRequest = getTestHttpRequest() + delete testHttpRequest.headers['x-slack-request-timestamp'] + delete testHttpRequest.headers['x-slack-signature'] + assert.equal(await slackValidateRequest(slackSigningSecret, testHttpRequest), false) + }) + }) + + describe('Using invalid slack app signing secret', function() { + it('should throw an error if it\'s undfined', async function() { + testHttpRequest = getTestHttpRequest() + assert.rejects(async () => { await slackValidateRequest(undefined, testHttpRequest) }) + }) + it('should throw an error if it\'s an empty string', async function() { + testHttpRequest = getTestHttpRequest() + assert.rejects(async () => { await slackValidateRequest('', testHttpRequest) }) + }) + it('should throw an error if it\'s a non-string', async function() { + testHttpRequest = getTestHttpRequest() + assert.rejects(async () => { await slackValidateRequest(12344, testHttpRequest) }) + }) + }) + + describe('Check validity of logging argument', function() { + it('should throw an error if logging is not a boolean', async function() { + testHttpRequest = getTestHttpRequest() + assert.rejects(async () => { await slackValidateRequest(slackSigningSecret, testHttpRequest, 1) }) + }) + }) + }) +} + +module.exports = runTests \ No newline at end of file diff --git a/test/validate-slack-request.js b/test/validate-slack-request.js deleted file mode 100644 index 80bcc7a..0000000 --- a/test/validate-slack-request.js +++ /dev/null @@ -1,127 +0,0 @@ -/* eslint-env mocha */ - -const slackValidateRequest = require('..') -var assert = require('assert') - -// Test object. Simulate an express request object -var slackSigningSecret = '8f742231b10e8888abcd99yyyzzz85a5' - -function getTestHttpRequest (textArgs, useGet=true) { - var req = { - 'headers': { - 'x-slack-request-timestamp': '1531420618', - 'x-slack-signature': textArgs ? 'v0=a3e650d30d1e91901834f91d048c9d3c0a50e4dcffcef7bc67884e95df8588ce' : 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', - }, - 'body': { - 'token': 'xyzz0WbapA4vBCDEFasx0q6G', - 'team_id': 'T1DC2JH3J', - 'team_domain': 'testteamnow', - 'channel_id': 'G8PSS9T3V', - 'channel_name': 'foobar', - 'user_id': 'U2CERLKJA', - 'user_name': 'roadrunner', - 'command': '/webhook-collect', - 'text': textArgs || '', - 'response_url': 'https://hooks.slack.com/commands/T1DC2JH3J/397700885554/96rGlfmibIGlgcZRskXaIFfN', - 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c' - }, - } - if (useGet) { - req.get = function (element) { - return req['headers'][element.toLowerCase()] - } - } - return req -} - -var testHttpRequest - -describe('Slack incoming request test', function () { - describe('Basic test', function () { - it('should return true with test object', function () { - assert.equal(slackValidateRequest(slackSigningSecret, getTestHttpRequest()), true) - }) - }) - - describe('Test nultiple args', function () { - it('should return true', function () { - assert.equal(slackValidateRequest(slackSigningSecret, getTestHttpRequest('args1 args2')), true) - }) - }) - - describe('Test special characters in command', function() { - it('should return true', function() { - testHttpRequest = getTestHttpRequest('(!)') - testHttpRequest.headers['x-slack-signature'] = 'v0=85b7bd32a59380ae4a50db6d76eed906f36daec1660ceced4907f44eaaf60757' - assert.equal(slackValidateRequest('slackSigningSecret', testHttpRequest), true) - }) - }) - - describe('Wrong signature', function () { - it('should return false if the signature doesn\'t match', function () { - testHttpRequest = getTestHttpRequest() - testHttpRequest.headers['x-slack-signature'] = 'v0=a2114d57b58eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503' - assert.equal(slackValidateRequest(slackSigningSecret, testHttpRequest), false) - }) - }) - - describe('Wrong Signing Secret', function () { - it('should return false if the signing secret is not the correct one', function () { - var tmpSlackSigningSecret = '9f742231b10e8888abcd99yyyzzz85a5' - assert.equal(slackValidateRequest(tmpSlackSigningSecret, getTestHttpRequest()), false) - }) - }) - - describe('Wrong Timestamp', function () { - it('should return false if the timestamp is wrong', function () { - testHttpRequest = getTestHttpRequest() - testHttpRequest.headers['x-slack-request-timestamp'] = '1531420619' - assert.equal(slackValidateRequest(slackSigningSecret, testHttpRequest), false) - }) - }) - - describe('Wrong body', function () { - it('should return false if the body is not the correct one', function () { - testHttpRequest = getTestHttpRequest() - testHttpRequest.body.text = 'test' - assert.equal(slackValidateRequest(slackSigningSecret, testHttpRequest), false) - }) - }) - - describe('Using an invalid slack request', function () { - it('should return false', function () { - testHttpRequest = getTestHttpRequest() - delete testHttpRequest.headers['x-slack-request-timestamp'] - delete testHttpRequest.headers['x-slack-signature'] - assert.equal(slackValidateRequest(slackSigningSecret, testHttpRequest), false) - }) - }) - - describe('Using invalid slack app signing secret', function() { - it('should throw an error if it\'s undfined', function() { - testHttpRequest = getTestHttpRequest() - assert.throws(() => { slackValidateRequest(undefined, testHttpRequest) }) - }) - it('should throw an error if it\'s an empty string', function() { - testHttpRequest = getTestHttpRequest() - assert.throws(() => { slackValidateRequest('', testHttpRequest) }) - }) - it('should throw an error if it\'s a non-string', function() { - testHttpRequest = getTestHttpRequest() - assert.throws(() => { slackValidateRequest(12344, testHttpRequest) }) - }) - }) - - describe('Check validity of logging argument', function() { - it('should throw an error if logging is not a boolean', function() { - testHttpRequest = getTestHttpRequest() - assert.throws(() => { slackValidateRequest(slackSigningSecret, testHttpRequest, 1) }) - }) - }) - - describe('Using a http.IncomingMessage for the request object', function() { - it('should function normally', function() { - assert.equal(slackValidateRequest(slackSigningSecret, getTestHttpRequest(undefined, false)), true) - }) - }) -}) From e6b4ebc5698b4955798c7702548355f581b50ea4 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 16:14:22 -0400 Subject: [PATCH 10/19] fix: correct httpReq variable typo --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 6103de9..e108df3 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,7 @@ function getBodyFromStream(httpReq) { httpReq.on('end', () => { resolve(bodyString) }) - http.on('error', err => { + httpReq.on('error', err => { reject(err) }) }) From 92b1ccfec05b32b5f6959f6d4f9e8037fe2ff251 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 16:15:16 -0400 Subject: [PATCH 11/19] test: add tests for standard http support --- test/express.test.js | 5 +++-- test/http.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ test/next.test.js | 5 +++-- test/util.js | 3 +-- 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 test/http.test.js diff --git a/test/express.test.js b/test/express.test.js index 8065fd5..e75b9c8 100644 --- a/test/express.test.js +++ b/test/express.test.js @@ -4,7 +4,7 @@ const runTests = require('./util') -function getTestHttpRequest (textArgs) { +function getTestHttpRequest (textArgs, bodyChanges) { return { 'headers': { 'x-slack-request-timestamp': '1531420618', @@ -21,7 +21,8 @@ function getTestHttpRequest (textArgs) { 'command': '/webhook-collect', 'text': textArgs || '', 'response_url': 'https://hooks.slack.com/commands/T1DC2JH3J/397700885554/96rGlfmibIGlgcZRskXaIFfN', - 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c' + 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c', + ...bodyChanges }, get: function (element) { return this.headers[element.toLowerCase()] diff --git a/test/http.test.js b/test/http.test.js new file mode 100644 index 0000000..ce8bf36 --- /dev/null +++ b/test/http.test.js @@ -0,0 +1,41 @@ +/** + * Tests for the standard http request object + */ + +const EventEmitter = require('events') +const { stringify } = require('querystring') + +const runTests = require('./util') +const eventEmitter = new EventEmitter() + +function getTestHttpRequest (textArgs, bodyChanges) { + const body = { + 'token': 'xyzz0WbapA4vBCDEFasx0q6G', + 'team_id': 'T1DC2JH3J', + 'team_domain': 'testteamnow', + 'channel_id': 'G8PSS9T3V', + 'channel_name': 'foobar', + 'user_id': 'U2CERLKJA', + 'user_name': 'roadrunner', + 'command': '/webhook-collect', + 'text': textArgs || '', + 'response_url': 'https://hooks.slack.com/commands/T1DC2JH3J/397700885554/96rGlfmibIGlgcZRskXaIFfN', + 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c', + ...bodyChanges + } + + process.nextTick(() => { + eventEmitter.emit('data', stringify(body)) + eventEmitter.emit('end') + }); + + return { + 'headers': { + 'x-slack-request-timestamp': '1531420618', + 'x-slack-signature': textArgs ? 'v0=a3e650d30d1e91901834f91d048c9d3c0a50e4dcffcef7bc67884e95df8588ce' : 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', + }, + on: (name, cb) => eventEmitter.on(name, cb) + } +} + +runTests('http', getTestHttpRequest) \ No newline at end of file diff --git a/test/next.test.js b/test/next.test.js index fa02b29..ff112c0 100644 --- a/test/next.test.js +++ b/test/next.test.js @@ -4,7 +4,7 @@ const runTests = require('./util') -function getTestHttpRequest (textArgs) { +function getTestHttpRequest (textArgs, bodyChanges) { return { 'headers': { 'x-slack-request-timestamp': '1531420618', @@ -21,7 +21,8 @@ function getTestHttpRequest (textArgs) { 'command': '/webhook-collect', 'text': textArgs || '', 'response_url': 'https://hooks.slack.com/commands/T1DC2JH3J/397700885554/96rGlfmibIGlgcZRskXaIFfN', - 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c' + 'trigger_id': '398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c', + ...bodyChanges }, } } diff --git a/test/util.js b/test/util.js index 817d099..e12148e 100644 --- a/test/util.js +++ b/test/util.js @@ -60,8 +60,7 @@ function runTests(frameworkName, getTestHttpRequest) { describe('Wrong body', function () { it('should return false if the body is not the correct one', async function () { - testHttpRequest = getTestHttpRequest() - testHttpRequest.body.text = 'test' + testHttpRequest = getTestHttpRequest(undefined, { text: 'test' }) assert.equal(await slackValidateRequest(slackSigningSecret, testHttpRequest), false) }) }) From 4458bb69de2d9e8a2dca8f97c9804eb15ded2c23 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:28:57 -0400 Subject: [PATCH 12/19] docs: update README to reflect Next.js and Node http support --- README.md | 70 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9de7185..2a2e6f0 100644 --- a/README.md +++ b/README.md @@ -2,54 +2,64 @@ # validate-slack-request -A simple module to validate Slack requests based on [this article](https://api.slack.com/docs/verifying-requests-from-slack). The module requires a valid expressJS request object as defined [here](https://expressjs.com/en/api.html#reqhttps://expressjs.com/en/api.html#req). See more about that in the [API section](#api) +A simple module to validate Slack requests passed in via an Express, Next, or a Node.js `http` endpoint handler function, based off of the specification included in [the official Slack guide](https://api.slack.com/docs/verifying-requests-from-slack) for request validation. -Disclaimer: this module is not developed nor endorsed by Slack +**Disclaimer**: this module is not developed nor endorsed by Slack. -# Installation - -To install use: +## Installation ```$ npm install validate-slack-request``` -Since Slack is sending requests using a POST with `application/x-www-form-urlencoded` encoded payload, the `express.urlencoded` module needs to be enabled in Express in order to parse the POST payload. To enable it, add the following to your main express script (e.g. `app.js` or `server.js`) +## Usage + +```javascript +const validateSlackRequest = require('validateSlackRequest') + +const signSecret = process.env.SLACK_SIGNING_SECRET + +const endpointHandler = async (req, res) => { // ← your endpoint handler + const isValid = await validateSlackRequest(signSecret, req) + + if (!isValid) { + // for Express & Next.js + res.status(403).send("Invalid Slack signing secret") + + // for the Node.js http module + res.statusCode = 403 + res.end("Invalid Slack signing secret") + return + } +} +``` +### For Express Users +If using Express, you must register the `express.urlencoded` middleware with your Express app: ```javascript -app.use(express.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded +const express = require("express") +const app = express() + +app.use(express.urlencoded({ extended: true })) // ← register it with your app ``` +See the [API section](#API) below to learn more about why we require an additional middleware. # API ```validateSlackRequest (slackAppSigningSecret, httpReq, logging)``` -Where: -* `slackAppSigningSecret`: this is the Slack Signing Secret assigned to the app when created. This can be accessed from the Slack app settings under "Basic Information". Look for "Signing secret". -* `httpReq`: Express request object as defined [here](https://expressjs.com/en/api.html#reqhttps://expressjs.com/en/api.html#req). If this module is used outside Express, make sure that `httpreq` exposes the following: - * `get()`: used to retrieve HTTP request headers (e.g. `httpReq.get('Content-Type')`) - * `.body` : JSON object representing the body of the HTTP POST request. -* `logging`: Optional parameter (default value is `false`) to print log information to console. +* `slackAppSigningSecret`: the [Slack Signing Secret](https://api.slack.com/authentication/verifying-requests-from-slack#about) assigned to your [Slack app](https://api.slack.com/authentication/verifying-requests-from-slack#about). We recommend storing this in an environment variable for security (see [Usage](#Usage) above). -# Example +* `httpReq`: the `req` parameter passed to your endpoint handler function. + - for **Express** users, this is a [Request](https://expressjs.com/en/api.html#req) object** + - for **Node.js** `http` module users, this is an [IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage) object + - for **Next.js** users, this is a [_modified_ IncomingMessage](https://nextjs.org/docs/api-routes/introduction) object + - _don't see your framework here?_ [Open an Issue 😃](https://github.com/gverni/validate-slack-request/issues/new/choose) -In express it can be added to your route using: - -``` -const slackValidateRequest = require('validate-slack-request') +* `logging`: Optional parameter (default value is `false`) to print log information to console. -... +\** Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#Usage) for guidance. -router.post('/', function (req, res, next) { - if (validateSlackRequest(process.env.SLACK_APP_SIGNING_SECRET, req)) { - // Valid request - Send appropriate response - res.send(...) - } - ... -} -``` - -Above example assumes that the signing secret is stored in environment variable `SLACK_APP_SIGNING_SECRET` (hardcoding of this variable is not advised) -# Errors +### Errors Following errors are thrown when invalid arguments are passed: From e3966b9134a8b2f9f0198b8a07ed1b09524c5dfc Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:31:04 -0400 Subject: [PATCH 13/19] docs: bold frameworks on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a2e6f0..4966265 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # validate-slack-request -A simple module to validate Slack requests passed in via an Express, Next, or a Node.js `http` endpoint handler function, based off of the specification included in [the official Slack guide](https://api.slack.com/docs/verifying-requests-from-slack) for request validation. +A simple module to validate Slack requests passed in via an **Express**, **Next**, or a **Node.js** `http` endpoint handler function, based off of the specification included in [the official Slack guide](https://api.slack.com/docs/verifying-requests-from-slack) for request validation. **Disclaimer**: this module is not developed nor endorsed by Slack. From 54f1ddf4b4a2104b1b742b3cc03deb13bd28a219 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:32:38 -0400 Subject: [PATCH 14/19] docs: clarify Usage code with added return --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4966265..cfab910 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ const endpointHandler = async (req, res) => { // ← your endpoint handler if (!isValid) { // for Express & Next.js res.status(403).send("Invalid Slack signing secret") + return // for the Node.js http module res.statusCode = 403 From 1342377f8154213ae1f6950957cbba64d3848f9d Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:33:24 -0400 Subject: [PATCH 15/19] docs: clarify README reference to middleware --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfab910..eb5b18f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ const app = express() app.use(express.urlencoded({ extended: true })) // ← register it with your app ``` -See the [API section](#API) below to learn more about why we require an additional middleware. +See the [API section](#API) below to learn more about why we require the `express.urlencoded` middleware. # API From 447453dd273380c84ce1f36b5156f6d3a1b499ae Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:34:02 -0400 Subject: [PATCH 16/19] docs: add "For Express Users" notice --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb5b18f..89b89f2 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ See the [API section](#API) below to learn more about why we require the `expres * `logging`: Optional parameter (default value is `false`) to print log information to console. -\** Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#Usage) for guidance. +\** **For Express Users:** Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#Usage) for guidance. ### Errors From ae0cc5dfbd34cc73fb3056d69da186df0d9715c9 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:36:04 -0400 Subject: [PATCH 17/19] docs: make "For Express Users" API notice a seperate section --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 89b89f2..38bdfd3 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ See the [API section](#API) below to learn more about why we require the `expres * `logging`: Optional parameter (default value is `false`) to print log information to console. -\** **For Express Users:** Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#Usage) for guidance. +### \** For Express Users: +Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#Usage) for guidance. ### Errors From 140cf30398c50e8c597373a4ee447068ad06b84c Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:36:42 -0400 Subject: [PATCH 18/19] docs: remove colon from README header --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 38bdfd3..7941352 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ See the [API section](#API) below to learn more about why we require the `expres * `logging`: Optional parameter (default value is `false`) to print log information to console. -### \** For Express Users: +### \** For Express Users Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#Usage) for guidance. From 253567939e0a1fd0b7cf84bf54bfe143c4fe2b61 Mon Sep 17 00:00:00 2001 From: Max Karpawich Date: Tue, 16 Jun 2020 17:38:22 -0400 Subject: [PATCH 19/19] docs: clarify README link to "For Express Users" section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7941352..85d9436 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ See the [API section](#API) below to learn more about why we require the `expres * `logging`: Optional parameter (default value is `false`) to print log information to console. ### \** For Express Users -Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#Usage) for guidance. +Slack sends POST requests with an `application/x-www-form-urlencoded` encoded payload. **Express users** must register the `express.urlencoded` middleware with their Express app so that `validate-slack-request` can access that payload. See the sample code provided above under [Usage](#For-Express-Users) for guidance. ### Errors