-
Notifications
You must be signed in to change notification settings - Fork 522
/
Copy pathwebhooks.js
220 lines (194 loc) · 7.25 KB
/
webhooks.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
'use strict';
var crypto = require('crypto');
var _ = require('lodash');
var scmp = require('scmp');
var url = require('url');
/**
Utility function to get the expected signature for a given request
@param {string} authToken - The auth token, as seen in the Twilio portal
@param {string} url - The full URL (with query string) you configured to handle this request
@param {object} params - the parameters sent with this request
@returns {string} - signature
*/
function getExpectedTwilioSignature(authToken, url, params) {
if (url.indexOf('bodySHA256') != -1) params = {};
var data = Object.keys(params)
.sort()
.reduce((acc, key) => acc + key + params[key], url);
return crypto
.createHmac('sha1', authToken)
.update(Buffer.from(data, 'utf-8'))
.digest('base64');
}
/**
Utility function to get the expected body hash for a given request's body
@param {string} body - The plain-text body of the request
*/
function getExpectedBodyHash(body) {
return crypto
.createHash('sha256')
.update(Buffer.from(body, 'utf-8'))
.digest('hex');
}
/**
Utility function to validate an incoming request is indeed from Twilio
@param {string} authToken - The auth token, as seen in the Twilio portal
@param {string} twilioHeader - The value of the X-Twilio-Signature header from the request
@param {string} url - The full URL (with query string) you configured to handle this request
@param {object} params - the parameters sent with this request
@returns {boolean} - valid
*/
function validateRequest(authToken, twilioHeader, url, params) {
twilioHeader = twilioHeader || '';
var expectedSignature = getExpectedTwilioSignature(authToken, url, params);
return scmp(Buffer.from(twilioHeader), Buffer.from(expectedSignature));
}
/**
Utility function to validate an incoming request is indeed from Twilio. This also validates
the request body against the bodySHA256 post parameter.
@param {string} authToken - The auth token, as seen in the Twilio portal
@param {string} twilioHeader - The value of the X-Twilio-Signature header from the request
@param {string} requestUrl - The full URL (with query string) you configured to handle this request
@param {string} body - The body of the request
@returns {boolean} - valid
*/
function validateRequestWithBody(authToken, twilioHeader, requestUrl, body) {
var urlObject = new url.URL(requestUrl);
return validateRequest(authToken, twilioHeader, requestUrl, {}) && validateBody(body, urlObject.searchParams.get('bodySHA256'));
}
function validateBody(body, bodyHash) {
var expectedHash = getExpectedBodyHash(body);
return scmp(Buffer.from(bodyHash), Buffer.from(expectedHash));
}
/**
Utility function to validate an incoming request is indeed from Twilio (for use with express).
adapted from https://github.com/crabasa/twiliosig
@param {object} request - An expressjs request object (http://expressjs.com/api.html#req.params)
@param {string} authToken - The auth token, as seen in the Twilio portal
@param {object} opts - options for request validation:
- url: The full URL (with query string) you used to configure the webhook with Twilio - overrides host/protocol options
- host: manually specify the host name used by Twilio in a number's webhook config
- protocol: manually specify the protocol used by Twilio in a number's webhook config
*/
function validateExpressRequest(request, authToken, opts) {
var options = opts || {};
var webhookUrl;
if (options.url) {
// Let the user specify the full URL
webhookUrl = options.url;
} else {
// Use configured host/protocol, or infer based on request
var protocol = options.protocol || request.protocol;
var host = options.host || request.headers.host;
webhookUrl = url.format({
protocol: protocol,
host: host,
pathname: request.originalUrl
});
if (request.originalUrl.search(/\?/) >= 0) {
webhookUrl = webhookUrl.replace("%3F","?");
}
}
if (webhookUrl.indexOf('bodySHA256') > 0) {
return validateRequestWithBody(
authToken,
request.header('X-Twilio-Signature'),
webhookUrl,
request.body || {}
);
} else {
return validateRequest(
authToken,
request.header('X-Twilio-Signature'),
webhookUrl,
request.body || {}
);
}
}
/**
Express middleware to accompany a Twilio webhook. Provides Twilio
request validation, and makes the response a little more friendly for our
TwiML generator. Request validation requires the express.urlencoded middleware
to have been applied (e.g. app.use(express.urlencoded()); in your app config).
Options:
- validate: {Boolean} whether or not the middleware should validate the request
came from Twilio. Default true. If the request does not originate from
Twilio, we will return a text body and a 403. If there is no configured
auth token and validate=true, this is an error condition, so we will return
a 500.
- host: manually specify the host name used by Twilio in a number's webhook config
- protocol: manually specify the protocol used by Twilio in a number's webhook config
Returns a middleware function.
Examples:
var webhookMiddleware = twilio.webhook();
var webhookMiddleware = twilio.webhook('asdha9dhjasd'); //init with auth token
var webhookMiddleware = twilio.webhook({
validate:false // don't attempt request validation
});
var webhookMiddleware = twilio.webhook({
host: 'hook.twilio.com',
protocol: 'https'
});
*/
function webhook() {
var opts = {
validate: true,
};
// Process arguments
var tokenString;
for (var i = 0, l = arguments.length; i < l; i++) {
var arg = arguments[i];
if (typeof arg === 'string') {
tokenString = arg;
} else {
opts = _.extend(opts, arg);
}
}
// set auth token from input or environment variable
opts.authToken = tokenString ? tokenString : process.env.TWILIO_AUTH_TOKEN;
// Create middleware function
return function hook(request, response, next) {
// Check if the 'X-Twilio-Signature' header exists or not
if (!request.header('X-Twilio-Signature')) {
return response.type('text/plain')
.status(400)
.send('No signature header error - X-Twilio-Signature header does not exist, maybe this request is not coming from Twilio.');
}
// Do validation if requested
if (opts.validate) {
// Check for a valid auth token
if (!opts.authToken) {
console.error('[Twilio]: Error - Twilio auth token is required for webhook request validation.');
response.type('text/plain')
.status(500)
.send('Webhook Error - we attempted to validate this request without first configuring our auth token.');
} else {
// Check that the request originated from Twilio
var valid = validateExpressRequest(request, opts.authToken, {
url: opts.url,
host: opts.host,
protocol: opts.protocol
});
if (valid) {
next();
} else {
return response
.type('text/plain')
.status(403)
.send('Twilio Request Validation Failed.');
}
}
} else {
next();
}
};
}
module.exports = {
getExpectedTwilioSignature,
getExpectedBodyHash,
validateRequest,
validateRequestWithBody,
validateExpressRequest,
validateBody,
webhook,
};