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

Commit 6f3b0ee

Browse files
authored
Merge pull request #71 from ianwsperber/feature/export-verify-request-signature
Export verifyRequestSignature
2 parents c5dc7f0 + 37e54f3 commit 6f3b0ee

File tree

5 files changed

+192
-126
lines changed

5 files changed

+192
-126
lines changed

docs/reference.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ If `options.includeHeaders` is truthy, then the SlackEventAdapter will emit an a
1919
with the event that has the parsed headers of the HTTP request. See SlackEventAdapter for more
2020
details.
2121

22+
#### verifyRequestSignature([_params_])
23+
24+
A helper method for verifying a request signature according to the docs here: https://api.slack.com/docs/verifying-requests-from-slack
25+
26+
The `params.signingSecret` is a required string parameter which you can find in your Slack app's Basic
27+
Information.
28+
29+
The `params.requestSignature` is a required string parameter taken from the 'x-slack-signature' header of the request.
30+
31+
The `params.requestTimestamp` is a required numeric parameter taken from the 'x-slack-request-timestamp' header of the request.
32+
33+
The `params.body` is a required string parameter for the raw, unparsed request body. If your application automatically parses JSON request you may need to retrieve the raw body prior to parsing.
34+
2235
### SlackEventAdapter
2336

2437
This object is responsible for consuming HTTP requests from the Slack Events API (via a request handler or

src/http-handler.js

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,44 @@ const responseStatuses = {
1717

1818
const debug = debugFactory('@slack/events-api:http-handler');
1919

20+
/**
21+
* Method to verify signature of requests
22+
*
23+
* @param {string} signingSecret - Signing secret used to verify request signature
24+
* @param {string} requestSignature - Signature from request 'x-slack-signature' header
25+
* @param {number} requestTimestamp - Timestamp from request 'x-slack-request-timestamp' header
26+
* @param {string} body - Raw body string
27+
* @returns {boolean} Indicates if request is verified
28+
*/
29+
export function verifyRequestSignature({
30+
signingSecret, requestSignature, requestTimestamp, body,
31+
}) {
32+
// Divide current date to match Slack ts format
33+
// Subtract 5 minutes from current time
34+
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - (60 * 5);
35+
36+
if (requestTimestamp < fiveMinutesAgo) {
37+
debug('request is older than 5 minutes');
38+
const error = new Error('Slack request signing verification failed');
39+
error.code = errorCodes.REQUEST_TIME_FAILURE;
40+
throw error;
41+
}
42+
43+
const hmac = crypto.createHmac('sha256', signingSecret);
44+
const [version, hash] = requestSignature.split('=');
45+
hmac.update(`${version}:${requestTimestamp}:${body}`);
46+
47+
if (hash !== hmac.digest('hex')) {
48+
debug('request signature is not valid');
49+
const error = new Error('Slack request signing verification failed');
50+
error.code = errorCodes.SIGNATURE_VERIFICATION_FAILURE;
51+
throw error;
52+
}
53+
54+
debug('request signing verification success');
55+
return true;
56+
}
57+
2058
export function createHTTPHandler(adapter) {
2159
const poweredBy = packageIdentifier();
2260

@@ -96,46 +134,6 @@ export function createHTTPHandler(adapter) {
96134
}
97135
}
98136

99-
/**
100-
* Method to verify signature of requests
101-
*
102-
* @param {string} signingSecret - Signing secret used to verify request signature
103-
* @param {Object} requestHeaders - Request headers
104-
* @param {string} body - Raw body string
105-
* @returns {boolean} Indicates if request is verified
106-
*/
107-
function verifyRequestSignature(signingSecret, requestHeaders, body) {
108-
// Request signature
109-
const signature = requestHeaders['x-slack-signature'];
110-
// Request timestamp
111-
const ts = requestHeaders['x-slack-request-timestamp'];
112-
113-
// Divide current date to match Slack ts format
114-
// Subtract 5 minutes from current time
115-
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - (60 * 5);
116-
117-
if (ts < fiveMinutesAgo) {
118-
debug('request is older than 5 minutes');
119-
const error = new Error('Slack request signing verification failed');
120-
error.code = errorCodes.REQUEST_TIME_FAILURE;
121-
throw error;
122-
}
123-
124-
const hmac = crypto.createHmac('sha256', signingSecret);
125-
const [version, hash] = signature.split('=');
126-
hmac.update(`${version}:${ts}:${body}`);
127-
128-
if (hash !== hmac.digest('hex')) {
129-
debug('request signature is not valid');
130-
const error = new Error('Slack request signing verification failed');
131-
error.code = errorCodes.SIGNATURE_VERIFICATION_FAILURE;
132-
throw error;
133-
}
134-
135-
debug('request signing verification success');
136-
return true;
137-
}
138-
139137
/**
140138
* Request listener used to handle Slack requests and send responses and
141139
* verify request signatures
@@ -153,7 +151,12 @@ export function createHTTPHandler(adapter) {
153151
.then((r) => {
154152
const rawBody = r.toString();
155153

156-
if (verifyRequestSignature(adapter.signingSecret, req.headers, rawBody)) {
154+
if (verifyRequestSignature({
155+
signingSecret: adapter.signingSecret,
156+
requestSignature: req.headers['x-slack-signature'],
157+
requestTimestamp: req.headers['x-slack-request-timestamp'],
158+
body: rawBody,
159+
})) {
157160
// Request signature is verified
158161
// Parse raw body
159162
const body = JSON.parse(rawBody);

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { errorCodes as adapterErrorCodes, SlackEventAdapter } from './adapter';
22

3+
export { verifyRequestSignature } from './http-handler';
34
export const errorCodes = adapterErrorCodes;
45

56
export function createEventAdapter(signingSecret, options) {

test/unit/test-http-handler.js

Lines changed: 116 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -7,114 +7,145 @@ var systemUnderTest = proxyquire('../../dist/http-handler', {
77
'raw-body': getRawBodyStub
88
});
99
var createHTTPHandler = systemUnderTest.createHTTPHandler;
10+
var verifyRequestSignature = systemUnderTest.verifyRequestSignature;
1011

1112
// fixtures
1213
var correctSigningSecret = 'SIGNING_SECRET';
1314
var correctRawBody = '{"type":"event_callback","event":{"type":"reaction_added",' +
1415
'"user":"U123","item":{"type":"message","channel":"C123"}}}';
1516

16-
describe('createHTTPHandler', function () {
17+
describe('http-handler', function () {
1718
beforeEach(function () {
18-
this.emit = sinon.stub();
19-
this.res = sinon.stub({
20-
setHeader: function () { },
21-
send: function () { },
22-
end: function () { }
23-
});
24-
this.next = sinon.stub();
2519
this.correctDate = Math.floor(Date.now() / 1000);
26-
this.requestListener = createHTTPHandler({
27-
signingSecret: correctSigningSecret,
28-
emit: this.emit
29-
});
3020
});
3121

32-
it('should verify a correct signing secret', function (done) {
33-
var emit = this.emit;
34-
var res = this.res;
35-
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
36-
emit.resolves({ status: 200 });
37-
getRawBodyStub.resolves(correctRawBody);
38-
res.end.callsFake(function () {
39-
assert.equal(res.statusCode, 200);
40-
done();
22+
describe('verifyRequestSignature', function () {
23+
it('should return true for a valid request', function () {
24+
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
25+
var isVerified = verifyRequestSignature({
26+
signingSecret: correctSigningSecret,
27+
requestTimestamp: req.headers['x-slack-request-timestamp'],
28+
requestSignature: req.headers['x-slack-signature'],
29+
body: req.body
30+
});
31+
32+
assert.isTrue(isVerified);
4133
});
42-
this.requestListener(req, res);
43-
});
4434

45-
it('should fail request signing verification with an incorrect signing secret', function (done) {
46-
var res = this.res;
47-
var req = createRequest('INVALID_SECRET', this.correctDate, correctRawBody);
48-
getRawBodyStub.resolves(correctRawBody);
49-
res.end.callsFake(function () {
50-
assert.equal(res.statusCode, 404);
51-
done();
35+
it('should throw for a request signed with a different secret', function () {
36+
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
37+
assert.throws(() => verifyRequestSignature({
38+
signingSecret: 'INVALID_SECRET',
39+
requestTimestamp: req.headers['x-slack-request-timestamp'],
40+
requestSignature: req.headers['x-slack-signature'],
41+
body: req.body
42+
}), 'Slack request signing verification failed');
5243
});
53-
this.requestListener(req, res);
5444
});
5545

56-
it('should fail request signing verification with old timestamp', function (done) {
57-
var res = this.res;
58-
var sixMinutesAgo = Math.floor(Date.now() / 1000) - (60 * 6);
59-
var req = createRequest(correctSigningSecret, sixMinutesAgo, correctRawBody);
60-
getRawBodyStub.resolves(correctRawBody);
61-
res.end.callsFake(function () {
62-
assert.equal(res.statusCode, 404);
63-
done();
46+
describe('createHTTPHandler', function () {
47+
beforeEach(function () {
48+
this.emit = sinon.stub();
49+
this.res = sinon.stub({
50+
setHeader: function () { },
51+
send: function () { },
52+
end: function () { }
53+
});
54+
this.next = sinon.stub();
55+
this.correctDate = Math.floor(Date.now() / 1000);
56+
this.requestListener = createHTTPHandler({
57+
signingSecret: correctSigningSecret,
58+
emit: this.emit
59+
});
6460
});
65-
this.requestListener(req, res);
66-
});
6761

68-
it('should handle unexpected error', function (done) {
69-
var res = this.res;
70-
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
71-
getRawBodyStub.rejects(new Error('test error'));
72-
res.end.callsFake(function (result) {
73-
assert.equal(res.statusCode, 500);
74-
assert.isUndefined(result);
75-
done();
62+
it('should verify a correct signing secret', function (done) {
63+
var emit = this.emit;
64+
var res = this.res;
65+
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
66+
emit.resolves({ status: 200 });
67+
getRawBodyStub.resolves(correctRawBody);
68+
res.end.callsFake(function () {
69+
assert.equal(res.statusCode, 200);
70+
done();
71+
});
72+
this.requestListener(req, res);
7673
});
77-
this.requestListener(req, res);
78-
});
7974

80-
it('should provide message with unexpected errors in development', function (done) {
81-
var res = this.res;
82-
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
83-
process.env.NODE_ENV = 'development';
84-
getRawBodyStub.rejects(new Error('test error'));
85-
res.end.callsFake(function (result) {
86-
assert.equal(res.statusCode, 500);
87-
assert.equal(result, 'test error');
88-
delete process.env.NODE_ENV;
89-
done();
75+
it('should fail request signing verification with an incorrect signing secret', function (done) {
76+
var res = this.res;
77+
var req = createRequest('INVALID_SECRET', this.correctDate, correctRawBody);
78+
getRawBodyStub.resolves(correctRawBody);
79+
res.end.callsFake(function () {
80+
assert.equal(res.statusCode, 404);
81+
done();
82+
});
83+
this.requestListener(req, res);
9084
});
91-
this.requestListener(req, res);
92-
});
9385

94-
it('should set an identification header in its responses', function (done) {
95-
var emit = this.emit;
96-
var res = this.res;
97-
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
98-
emit.resolves({ status: 200 });
99-
getRawBodyStub.resolves(correctRawBody);
100-
res.end.callsFake(function () {
101-
assert(res.setHeader.calledWith('X-Slack-Powered-By'));
102-
done();
86+
it('should fail request signing verification with old timestamp', function (done) {
87+
var res = this.res;
88+
var sixMinutesAgo = Math.floor(Date.now() / 1000) - (60 * 6);
89+
var req = createRequest(correctSigningSecret, sixMinutesAgo, correctRawBody);
90+
getRawBodyStub.resolves(correctRawBody);
91+
res.end.callsFake(function () {
92+
assert.equal(res.statusCode, 404);
93+
done();
94+
});
95+
this.requestListener(req, res);
96+
});
97+
98+
it('should handle unexpected error', function (done) {
99+
var res = this.res;
100+
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
101+
getRawBodyStub.rejects(new Error('test error'));
102+
res.end.callsFake(function (result) {
103+
assert.equal(res.statusCode, 500);
104+
assert.isUndefined(result);
105+
done();
106+
});
107+
this.requestListener(req, res);
108+
});
109+
110+
it('should provide message with unexpected errors in development', function (done) {
111+
var res = this.res;
112+
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
113+
process.env.NODE_ENV = 'development';
114+
getRawBodyStub.rejects(new Error('test error'));
115+
res.end.callsFake(function (result) {
116+
assert.equal(res.statusCode, 500);
117+
assert.equal(result, 'test error');
118+
delete process.env.NODE_ENV;
119+
done();
120+
});
121+
this.requestListener(req, res);
122+
});
123+
124+
it('should set an identification header in its responses', function (done) {
125+
var emit = this.emit;
126+
var res = this.res;
127+
var req = createRequest(correctSigningSecret, this.correctDate, correctRawBody);
128+
emit.resolves({ status: 200 });
129+
getRawBodyStub.resolves(correctRawBody);
130+
res.end.callsFake(function () {
131+
assert(res.setHeader.calledWith('X-Slack-Powered-By'));
132+
done();
133+
});
134+
this.requestListener(req, res);
103135
});
104-
this.requestListener(req, res);
105-
});
106136

107-
it('should respond to url verification requests', function (done) {
108-
var res = this.res;
109-
var emit = this.emit;
110-
var urlVerificationBody = '{"type":"url_verification","challenge": "TEST_CHALLENGE"}';
111-
var req = createRequest(correctSigningSecret, this.correctDate, urlVerificationBody);
112-
getRawBodyStub.resolves(urlVerificationBody);
113-
res.end.callsFake(function () {
114-
assert(emit.notCalled);
115-
assert.equal(res.statusCode, 200);
116-
done();
137+
it('should respond to url verification requests', function (done) {
138+
var res = this.res;
139+
var emit = this.emit;
140+
var urlVerificationBody = '{"type":"url_verification","challenge": "TEST_CHALLENGE"}';
141+
var req = createRequest(correctSigningSecret, this.correctDate, urlVerificationBody);
142+
getRawBodyStub.resolves(urlVerificationBody);
143+
res.end.callsFake(function () {
144+
assert(emit.notCalled);
145+
assert.equal(res.statusCode, 200);
146+
done();
147+
});
148+
this.requestListener(req, res);
117149
});
118-
this.requestListener(req, res);
119150
});
120151
});

test/unit/test-index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
var assert = require('chai').assert;
4+
var library = require('../../dist');
5+
6+
describe('@slack/events-api', function () {
7+
it('should export "verifyRequestSignature"', function () {
8+
assert.property(library, 'verifyRequestSignature');
9+
});
10+
11+
it('should export "createEventAdapter"', function () {
12+
assert.property(library, 'createEventAdapter');
13+
});
14+
15+
it('should export "errorCodes"', function () {
16+
assert.property(library, 'errorCodes');
17+
});
18+
});

0 commit comments

Comments
 (0)