Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #192 ExpressReceiver support for rawBody for signature verification #197

Merged
merged 2 commits into from
May 24, 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
2 changes: 1 addition & 1 deletion src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default class App {

// Check for required arguments of ExpressReceiver
if (signingSecret !== undefined) {
this.receiver = new ExpressReceiver({ signingSecret, endpoints });
this.receiver = new ExpressReceiver({ signingSecret, logger, endpoints });
} else if (receiver === undefined) {
// Check for custom receiver
throw errorWithCode(
Expand Down
204 changes: 204 additions & 0 deletions src/ExpressReceiver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// tslint:disable:no-implicit-dependencies
import 'mocha';
import { assert } from 'chai';
import { Request, Response } from 'express';
import { verifySignatureAndParseBody } from './ExpressReceiver';
import sinon, { SinonFakeTimers } from 'sinon';
import { Readable } from 'stream';
import { Logger, LogLevel } from '@slack/logger';

describe('ExpressReceiver', () => {

const noopLogger: Logger = {
debug(..._msg: any[]): void { },
info(..._msg: any[]): void { },
warn(..._msg: any[]): void { },
error(..._msg: any[]): void { },
setLevel(_level: LogLevel): void { },
setName(_name: string): void { },
};

describe('verifySignatureAndParseBody', () => {

let clock: SinonFakeTimers;

beforeEach(function () {
// requestTimestamp = 1531420618 means this timestamp
clock = sinon.useFakeTimers(new Date('Thu Jul 12 2018 11:36:58 GMT-0700').getTime());
});

afterEach(function () {
clock.restore();
});

// These values are example data in the official doc
// https://api.slack.com/docs/verifying-requests-from-slack
const signingSecret = '8f742231b10e8888abcd99yyyzzz85a5';
const signature = 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503';
const requestTimestamp = 1531420618;
const body = 'token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c';

function buildExpressRequest(): Request {
const reqAsStream = new Readable();
reqAsStream.push(body);
reqAsStream.push(null); // indicate EOF
(reqAsStream as { [key: string]: any }).headers = {
'x-slack-signature': signature,
'x-slack-request-timestamp': requestTimestamp
};
const req = reqAsStream as Request;
return req;
}

function buildGCPRequest(): Request {
const untypedReq: { [key: string]: any } = {
rawBody: body,
headers: {
'x-slack-signature': signature,
'x-slack-request-timestamp': requestTimestamp
}
};
const req = untypedReq as Request;
return req;
}

// ----------------------------
// runWithValidRequest

async function runWithValidRequest(req: Request, errorResult: any) {
// Arrange
const resp = {} as Response;
const next = (error: any) => { errorResult = error; };

// Act
const verifier = verifySignatureAndParseBody(noopLogger, signingSecret);
await verifier(req, resp, next);
return errorResult;
}

it('should verify requests', async () => {
let errorResult: any;
runWithValidRequest(buildExpressRequest(), errorResult);
// Assert
assert.isUndefined(errorResult);
});

it('should verify requests on GCP', async () => {
let errorResult: any;
runWithValidRequest(buildGCPRequest(), errorResult);
// Assert
assert.isUndefined(errorResult);
});

// ----------------------------
// verifyMissingHeaderDetection

function verifyMissingHeaderDetection(req: Request): Promise<any> {
// Arrange
const resp = {} as Response;
let errorResult: any;
const next = (error: any) => { errorResult = error; };

// Act
const verifier = verifySignatureAndParseBody(noopLogger, signingSecret);
return verifier(req, resp, next).then((_: any) => {
// Assert
assert.equal(errorResult, 'Error: Slack request signing verification failed. Some headers are missing.');
})
}

it('should detect headers missing', async () => {
const reqAsStream = new Readable();
reqAsStream.push(body);
reqAsStream.push(null); // indicate EOF
(reqAsStream as { [key: string]: any }).headers = {
'x-slack-signature': signature /*,
'x-slack-request-timestamp': requestTimestamp */
};
await verifyMissingHeaderDetection(reqAsStream as Request);
});

it('should detect headers missing on GCP', async () => {
const untypedReq: { [key: string]: any } = {
rawBody: body,
headers: {
'x-slack-signature': signature /*,
'x-slack-request-timestamp': requestTimestamp */
}
};
await verifyMissingHeaderDetection(untypedReq as Request);
});

// ----------------------------
// verifyTooOldTimestampError

function verifyTooOldTimestampError(req: Request): Promise<any> {
// Arrange
// restore the valid clock
clock.restore();

const resp = {} as Response;
let errorResult: any;
const next = (error: any) => { errorResult = error; };

// Act
const verifier = verifySignatureAndParseBody(noopLogger, signingSecret);
return verifier(req, resp, next).then((_: any) => {
// Assert
assert.equal(errorResult, 'Error: Slack request signing verification failed. Timestamp is too old.');
});
}

it('should detect too old tiestamp', async () => {
await verifyTooOldTimestampError(buildExpressRequest());
});

it('should detect too old tiestamp on GCP', async () => {
await verifyTooOldTimestampError(buildGCPRequest());
});

// ----------------------------
// verifySingnatureMismatch

function verifySingnatureMismatch(req: Request): Promise<any> {
// Arrange
const resp = {} as Response;
let errorResult: any;
const next = (error: any) => { errorResult = error; };

// Act
const verifier = verifySignatureAndParseBody(noopLogger, signingSecret);
verifier(req, resp, next);
return verifier(req, resp, next).then((_: any) => {
// Assert
assert.equal(errorResult, 'Error: Slack request signing verification failed. Signature mismatch.');
});
}

it('should detect signature mismatch', async () => {
const reqAsStream = new Readable();
reqAsStream.push(body);
reqAsStream.push(null); // indicate EOF
(reqAsStream as { [key: string]: any }).headers = {
'x-slack-signature': signature,
'x-slack-request-timestamp': requestTimestamp + 10
};
const req = reqAsStream as Request;
await verifySingnatureMismatch(req);
});

it('should detect signature mismatch on GCP', async () => {
const untypedReq: { [key: string]: any } = {
rawBody: body,
headers: {
'x-slack-signature': signature,
'x-slack-request-timestamp': requestTimestamp + 10
}
};
const req = untypedReq as Request;
await verifySingnatureMismatch(req);
});

});

});
Loading