1 | 'use strict';
|
2 |
|
3 | const crypto = require('crypto');
|
4 |
|
5 | const utils = require('./utils');
|
6 | const {StripeError, StripeSignatureVerificationError} = require('./Error');
|
7 |
|
8 | const Webhook = {
|
9 | DEFAULT_TOLERANCE: 300,
|
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 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
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 |
|
60 | const 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 |
|
74 |
|
75 |
|
76 |
|
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 |
|
145 | function 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 |
|
171 | Webhook.signature = signature;
|
172 |
|
173 | module.exports = Webhook;
|