UNPKG

4.63 kBJavaScriptView Raw
1'use strict';
2
3const crypto = require('crypto');
4
5const utils = require('./utils');
6const {StripeError, StripeSignatureVerificationError} = require('./Error');
7
8const Webhook = {
9 DEFAULT_TOLERANCE: 300, // 5 minutes
10
11 constructEvent(payload, header, secret, tolerance) {
12 this.signature.verifyHeader(
13 payload,
14 header,
15 secret,
16 tolerance || Webhook.DEFAULT_TOLERANCE
17 );
18
19 const jsonPayload = JSON.parse(payload);
20 return jsonPayload;
21 },
22
23 /**
24 * Generates a header to be used for webhook mocking
25 *
26 * @typedef {object} opts
27 * @property {number} timestamp - Timestamp of the header. Defaults to Date.now()
28 * @property {string} payload - JSON stringified payload object, containing the 'id' and 'object' parameters
29 * @property {string} secret - Stripe webhook secret 'whsec_...'
30 * @property {string} scheme - Version of API to hit. Defaults to 'v1'.
31 * @property {string} signature - Computed webhook signature
32 */
33 generateTestHeaderString: function(opts) {
34 if (!opts) {
35 throw new StripeError({
36 message: 'Options are required',
37 });
38 }
39
40 opts.timestamp =
41 Math.floor(opts.timestamp) || Math.floor(Date.now() / 1000);
42 opts.scheme = opts.scheme || signature.EXPECTED_SCHEME;
43
44 opts.signature =
45 opts.signature ||
46 signature._computeSignature(
47 opts.timestamp + '.' + opts.payload,
48 opts.secret
49 );
50
51 var generatedHeader = [
52 't=' + opts.timestamp,
53 opts.scheme + '=' + opts.signature,
54 ].join(',');
55
56 return generatedHeader;
57 },
58};
59
60const signature = {
61 EXPECTED_SCHEME: 'v1',
62
63 _computeSignature: (payload, secret) => {
64 return crypto
65 .createHmac('sha256', secret)
66 .update(payload, 'utf8')
67 .digest('hex');
68 },
69
70 verifyHeader(payload, header, secret, tolerance) {
71 payload = Buffer.isBuffer(payload) ? payload.toString('utf8') : payload;
72
73 // Express's type for `Request#headers` is `string | []string`
74 // which is because the `set-cookie` header is an array,
75 // but no other headers are an array (docs: https://nodejs.org/api/http.html#http_message_headers)
76 // (Express's Request class is an extension of http.IncomingMessage, and doesn't appear to be relevantly modified: https://github.com/expressjs/express/blob/master/lib/request.js#L31)
77 if (Array.isArray(header)) {
78 throw new Error(
79 'Unexpected: An array was passed as a header, which should not be possible for the stripe-signature header.'
80 );
81 }
82
83 header = Buffer.isBuffer(header) ? header.toString('utf8') : header;
84
85 const details = parseHeader(header, this.EXPECTED_SCHEME);
86
87 if (!details || details.timestamp === -1) {
88 throw new StripeSignatureVerificationError({
89 message: 'Unable to extract timestamp and signatures from header',
90 detail: {
91 header,
92 payload,
93 },
94 });
95 }
96
97 if (!details.signatures.length) {
98 throw new StripeSignatureVerificationError({
99 message: 'No signatures found with expected scheme',
100 detail: {
101 header,
102 payload,
103 },
104 });
105 }
106
107 const expectedSignature = this._computeSignature(
108 `${details.timestamp}.${payload}`,
109 secret
110 );
111
112 const signatureFound = !!details.signatures.filter(
113 utils.secureCompare.bind(utils, expectedSignature)
114 ).length;
115
116 if (!signatureFound) {
117 throw new StripeSignatureVerificationError({
118 message:
119 'No signatures found matching the expected signature for payload.' +
120 ' Are you passing the raw request body you received from Stripe?' +
121 ' https://github.com/stripe/stripe-node#webhook-signing',
122 detail: {
123 header,
124 payload,
125 },
126 });
127 }
128
129 const timestampAge = Math.floor(Date.now() / 1000) - details.timestamp;
130
131 if (tolerance > 0 && timestampAge > tolerance) {
132 throw new StripeSignatureVerificationError({
133 message: 'Timestamp outside the tolerance zone',
134 detail: {
135 header,
136 payload,
137 },
138 });
139 }
140
141 return true;
142 },
143};
144
145function parseHeader(header, scheme) {
146 if (typeof header !== 'string') {
147 return null;
148 }
149
150 return header.split(',').reduce(
151 (accum, item) => {
152 const kv = item.split('=');
153
154 if (kv[0] === 't') {
155 accum.timestamp = kv[1];
156 }
157
158 if (kv[0] === scheme) {
159 accum.signatures.push(kv[1]);
160 }
161
162 return accum;
163 },
164 {
165 timestamp: -1,
166 signatures: [],
167 }
168 );
169}
170
171Webhook.signature = signature;
172
173module.exports = Webhook;