This repository has been archived by the owner on Apr 11, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 390
/
validate.ts
124 lines (111 loc) · 3.27 KB
/
validate.ts
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
import {
abstractConvertRequest,
getHeader,
Headers,
NormalizedRequest,
} from '../../runtime/http';
import {createSHA256HMAC} from '../../runtime/crypto';
import {HashFormat} from '../../runtime/crypto/types';
import {ShopifyHeader} from '../types';
import {ConfigInterface} from '../base-types';
import {safeCompare} from '../auth/oauth/safe-compare';
import {logger} from '../logger';
import {
WebhookFields,
WebhookValidateParams,
WebhookValidation,
WebhookValidationErrorReason,
} from './types';
import {topicForStorage} from './registry';
const HANDLER_PROPERTIES: {
[property in keyof WebhookFields]: ShopifyHeader;
} = {
apiVersion: ShopifyHeader.ApiVersion,
domain: ShopifyHeader.Domain,
hmac: ShopifyHeader.Hmac,
topic: ShopifyHeader.Topic,
webhookId: ShopifyHeader.WebhookId,
};
export function validateFactory(config: ConfigInterface) {
return async function validate({
rawBody,
...adapterArgs
}: WebhookValidateParams): Promise<WebhookValidation> {
const request: NormalizedRequest = await abstractConvertRequest(
adapterArgs,
);
const log = logger(config);
const webhookCheck = checkWebhookRequest(rawBody, request.headers);
if (!webhookCheck.valid) {
await log.debug('Received malformed webhook request', webhookCheck);
return webhookCheck;
}
const {hmac, valid: _valid, ...loggingContext} = webhookCheck;
await log.debug('Webhook request is well formed', loggingContext);
if (await checkWebhookHmac(config.apiSecretKey, rawBody, hmac)) {
await log.debug('Webhook request is valid', loggingContext);
return webhookCheck;
} else {
await log.debug('Webhook validation failed', loggingContext);
if (config.isCustomStoreApp) {
log.deprecated(
'8.0.0',
"apiSecretKey should be set to the custom store app's API secret key, so that webhook validation succeeds. adminApiAccessToken should be set to the custom store app's Admin API access token",
);
}
return {
valid: false,
reason: WebhookValidationErrorReason.InvalidHmac,
};
}
};
}
function checkWebhookRequest(
rawBody: string,
headers: Headers,
): WebhookValidation {
if (!rawBody.length) {
return {
valid: false,
reason: WebhookValidationErrorReason.MissingBody,
};
}
const missingHeaders: ShopifyHeader[] = [];
const headerValues: WebhookFields = Object.entries(HANDLER_PROPERTIES).reduce(
(acc, [property, headerName]: [keyof WebhookFields, ShopifyHeader]) => {
const headerValue = getHeader(headers, headerName);
if (headerValue) {
acc[property] = headerValue;
} else {
missingHeaders.push(headerName);
}
return acc;
},
{} as WebhookFields,
);
if (missingHeaders.length) {
return {
valid: false,
reason: WebhookValidationErrorReason.MissingHeaders,
missingHeaders,
};
} else {
return {
valid: true,
...headerValues,
topic: topicForStorage(headerValues.topic),
};
}
}
async function checkWebhookHmac(
secret: string,
rawBody: string,
hmac: string,
): Promise<boolean> {
const generatedHash = await createSHA256HMAC(
secret,
rawBody,
HashFormat.Base64,
);
return safeCompare(generatedHash, hmac);
}