Skip to content
This repository has been archived by the owner on Jul 24, 2019. It is now read-only.

Adding support for pre-parsed requests #90

Merged
merged 3 commits into from
Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions src/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import { createHTTPHandler } from './http-handler';

const debug = debugFactory('@slack/events-api:adapter');

export const errorCodes = {
BODY_PARSER_NOT_PERMITTED: 'SLACKADAPTER_BODY_PARSER_NOT_PERMITTED_FAILURE',
};

export class SlackEventAdapter extends EventEmitter {
constructor(signingSecret, options = {}) {
if (!isString(signingSecret)) {
Expand Down Expand Up @@ -69,14 +65,7 @@ export class SlackEventAdapter extends EventEmitter {

expressMiddleware(middlewareOptions = {}) {
const requestListener = this.requestListener(middlewareOptions);
return (req, res, next) => {
// If parser is being used, we can't verify request signature
if (req.body) {
const error = new Error('Parsing request body prohibits request signature verification');
error.code = errorCodes.BODY_PARSER_NOT_PERMITTED;
next(error);
return;
}
return (req, res, next) => { // eslint-disable-line no-unused-vars
requestListener(req, res);
};
}
Expand Down
30 changes: 27 additions & 3 deletions src/http-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { packageIdentifier } from './util';
export const errorCodes = {
SIGNATURE_VERIFICATION_FAILURE: 'SLACKHTTPHANDLER_REQUEST_SIGNATURE_VERIFICATION_FAILURE',
REQUEST_TIME_FAILURE: 'SLACKHTTPHANDLER_REQUEST_TIMELIMIT_FAILURE',
BODY_PARSER_NOT_PERMITTED: 'SLACKADAPTER_BODY_PARSER_NOT_PERMITTED_FAILURE', // moved constant from adapter
};

const responseStatuses = {
Expand Down Expand Up @@ -36,7 +37,7 @@ export function verifyRequestSignature({

if (requestTimestamp < fiveMinutesAgo) {
debug('request is older than 5 minutes');
const error = new Error('Slack request signing verification failed');
const error = new Error('Slack request signing verification outdated');
error.code = errorCodes.REQUEST_TIME_FAILURE;
throw error;
}
Expand Down Expand Up @@ -148,10 +149,33 @@ export function createHTTPHandler(adapter) {
// Bind a response function to this request's respond object.
const respond = sendResponse(res);

getRawBody(req)
// If parser is being used and we don't receive the raw payload via `rawBody`,
// we can't verify request signature
if (req.body && !req.rawBody) {
const error = new Error('Parsing request body prohibits request signature verification');
error.code = errorCodes.BODY_PARSER_NOT_PERMITTED;
handleError(error, respond);
return;
}

// Some serverless cloud providers (e.g. Google Firebase Cloud Functions) might populate
// the request with a bodyparser before it can be populated by the SDK.
// To prevent throwing an error here, we check the `rawBody` field before parsing the request
// through the `raw-body` module (see Issue #85 - https://github.com/slackapi/node-slack-events-api/issues/85)
let parseRawBody;
if (req.rawBody) {
debug('Parsing request with a rawBody attribute');
parseRawBody = new Promise((resolve) => {
resolve(req.rawBody);
});
} else {
debug('Parsing raw request');
parseRawBody = getRawBody(req);
}

parseRawBody
.then((r) => {
const rawBody = r.toString();

if (verifyRequestSignature({
signingSecret: adapter.signingSecret,
requestSignature: req.headers['x-slack-signature'],
Expand Down
22 changes: 21 additions & 1 deletion test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,26 @@ function createRequest(signingSecret, ts, rawBody) {
'content-type': 'application/json'
};
return {
body: rawBody,
headers: headers
};
}

/**
* Creates request object with proper headers and a rawBody field payload
* @param {string} signingSecret - A Slack signing secret for request verification
* @param {Integer} ts - A timestamp for request verification and header
* @param {string} rawBody - String of raw body to be put in rawBody field
* @returns {Object} pseudo request object
*/
function createRawBodyRequest(signingSecret, ts, rawBody) {
const signature = createRequestSignature(signingSecret, ts, rawBody);
const headers = {
'x-slack-signature': signature,
'x-slack-request-timestamp': ts,
'content-type': 'application/json'
};
return {
rawBody: Buffer.from(rawBody),
headers: headers
};
}
Expand Down Expand Up @@ -79,6 +98,7 @@ function completionAggregator(done, totalParts) {
}

module.exports.createRequest = createRequest;
module.exports.createRawBodyRequest = createRawBodyRequest;
module.exports.createRequestSignature = createRequestSignature;
module.exports.createStreamRequest = createStreamRequest;
exports.completionAggregator = completionAggregator;
12 changes: 0 additions & 12 deletions test/unit/test-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ var getRandomPort = require('get-random-port');
var EventEmitter = require('events');
var systemUnderTest = require('../../dist/adapter');
var createStreamRequest = require('../helpers').createStreamRequest;
var errorCodes = systemUnderTest.errorCodes;
var SlackEventAdapter = systemUnderTest.default;

// fixtures and test helpers
Expand Down Expand Up @@ -81,17 +80,6 @@ describe('SlackEventAdapter', function () {
var middleware = this.adapter.expressMiddleware();
assert.isFunction(middleware);
});
it('should error when body parser is used', function (done) {
var middleware = this.adapter.expressMiddleware();
var req = { body: { } };
var res = this.res;
var next = this.next;
next.callsFake(function (err) {
assert.equal(err.code, errorCodes.BODY_PARSER_NOT_PERMITTED);
done();
});
middleware(req, res, next);
});
it('should emit on the adapter', function (done) {
var middleware = this.adapter.expressMiddleware();
var emit = this.emit;
Expand Down
51 changes: 44 additions & 7 deletions test/unit/test-http-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var assert = require('chai').assert;
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var createRequest = require('../helpers').createRequest;
var createRawBodyRequest = require('../helpers').createRawBodyRequest;
var getRawBodyStub = sinon.stub();
var systemUnderTest = proxyquire('../../dist/http-handler', {
'raw-body': getRawBodyStub
Expand All @@ -26,7 +27,7 @@ describe('http-handler', function () {
signingSecret: correctSigningSecret,
requestTimestamp: req.headers['x-slack-request-timestamp'],
requestSignature: req.headers['x-slack-signature'],
body: req.body
body: correctRawBody
});

assert.isTrue(isVerified);
Expand All @@ -38,7 +39,7 @@ describe('http-handler', function () {
signingSecret: 'INVALID_SECRET',
requestTimestamp: req.headers['x-slack-request-timestamp'],
requestSignature: req.headers['x-slack-signature'],
body: req.body
body: correctRawBody
}), 'Slack request signing verification failed');
});
});
Expand All @@ -64,18 +65,54 @@ describe('http-handler', function () {
var res = this.res;
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
emit.resolves({ status: 200 });
getRawBodyStub.resolves(correctRawBody);
getRawBodyStub.resolves(Buffer.from(correctRawBody));
res.end.callsFake(function () {
assert.equal(res.statusCode, 200);
done();
});
this.requestListener(req, res);
});

it('should verify a correct signing secret for a request with rawBody attribute', function (done) {
var emit = this.emit;
var res = this.res;
var req = createRawBodyRequest(correctSigningSecret, this.correctDate, correctRawBody);
emit.resolves({ status: 200 });
getRawBodyStub.resolves(Buffer.from(correctRawBody));
res.end.callsFake(function () {
assert.equal(res.statusCode, 200);
done();
});
this.requestListener(req, res);
});

it('should fail request signing verification for a request with a body but no rawBody', function (done) {
var res = this.res;
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
req.body = {};
getRawBodyStub.resolves(Buffer.from(correctRawBody));
res.end.callsFake(function () {
assert.equal(res.statusCode, 500);
done();
});
this.requestListener(req, res);
});

it('should fail request signing verification with an incorrect signing secret', function (done) {
var res = this.res;
var req = createRequest('INVALID_SECRET', this.correctDate, correctRawBody);
getRawBodyStub.resolves(correctRawBody);
getRawBodyStub.resolves(Buffer.from(correctRawBody));
res.end.callsFake(function () {
assert.equal(res.statusCode, 404);
done();
});
this.requestListener(req, res);
});

it('should fail request signing verification when a request has body and no rawBody attribute', function (done) {
var res = this.res;
var req = createRequest('INVALID_SECRET', this.correctDate, correctRawBody);
getRawBodyStub.resolves(Buffer.from(correctRawBody));
res.end.callsFake(function () {
assert.equal(res.statusCode, 404);
done();
Expand All @@ -87,7 +124,7 @@ describe('http-handler', function () {
var res = this.res;
var sixMinutesAgo = Math.floor(Date.now() / 1000) - (60 * 6);
var req = createRequest(correctSigningSecret, sixMinutesAgo, correctRawBody);
getRawBodyStub.resolves(correctRawBody);
getRawBodyStub.resolves(Buffer.from(correctRawBody));
res.end.callsFake(function () {
assert.equal(res.statusCode, 404);
done();
Expand Down Expand Up @@ -126,7 +163,7 @@ describe('http-handler', function () {
var res = this.res;
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
emit.resolves({ status: 200 });
getRawBodyStub.resolves(correctRawBody);
getRawBodyStub.resolves(Buffer.from(correctRawBody));
res.end.callsFake(function () {
assert(res.setHeader.calledWith('X-Slack-Powered-By'));
done();
Expand All @@ -139,7 +176,7 @@ describe('http-handler', function () {
var emit = this.emit;
var urlVerificationBody = '{"type":"url_verification","challenge": "TEST_CHALLENGE"}';
var req = createRequest(correctSigningSecret, this.correctDate, urlVerificationBody);
getRawBodyStub.resolves(urlVerificationBody);
getRawBodyStub.resolves(Buffer.from(urlVerificationBody));
res.end.callsFake(function () {
assert(emit.notCalled);
assert.equal(res.statusCode, 200);
Expand Down