-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathsaleor-webhook-validator.ts
221 lines (178 loc) · 6.71 KB
/
saleor-webhook-validator.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
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
221
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
import { APL } from "@/APL";
import { createDebug } from "@/debug";
import { fetchRemoteJwks } from "@/fetch-remote-jwks";
import { getOtelTracer } from "@/open-telemetry";
import { SaleorSchemaVersion } from "@/types";
import { parseSchemaVersion } from "@/util";
import { verifySignatureWithJwks } from "@/verify-signature";
import { PlatformAdapterInterface } from "./generic-adapter-use-case-types";
import { SaleorRequestProcessor } from "./saleor-request-processor";
import { WebhookContext, WebhookError } from "./saleor-webhook";
type WebhookValidationResult<TPayload> =
| { result: "ok"; context: WebhookContext<TPayload> }
| { result: "failure"; error: WebhookError | Error };
export class SaleorWebhookValidator {
private debug = createDebug("processProtectedHandler");
private tracer = getOtelTracer();
async validateRequest<TPayload, TRequestType>(config: {
allowedEvent: string;
apl: APL;
adapter: PlatformAdapterInterface<TRequestType>;
requestProcessor: SaleorRequestProcessor<TRequestType>;
}): Promise<WebhookValidationResult<TPayload>> {
try {
const context = await this.validateRequestOrThrowError<TPayload, TRequestType>(config);
return {
result: "ok",
context,
};
} catch (err) {
return {
result: "failure",
error: err as WebhookError | Error,
};
}
}
private async validateRequestOrThrowError<TPayload, TRequestType>({
allowedEvent,
apl,
adapter,
requestProcessor,
}: {
allowedEvent: string;
apl: APL;
adapter: PlatformAdapterInterface<TRequestType>;
requestProcessor: SaleorRequestProcessor<TRequestType>;
}): Promise<WebhookContext<TPayload>> {
return this.tracer.startActiveSpan(
"processSaleorWebhook",
{
kind: SpanKind.INTERNAL,
attributes: {
allowedEvent,
},
},
async (span) => {
try {
this.debug("Request processing started");
if (adapter.method !== "POST") {
this.debug("Wrong HTTP method");
throw new WebhookError("Wrong request method, only POST allowed", "WRONG_METHOD");
}
const { event, signature, saleorApiUrl } = requestProcessor.getSaleorHeaders();
const baseUrl = adapter.getBaseUrl();
if (!baseUrl) {
this.debug("Missing host header");
throw new WebhookError("Missing host header", "MISSING_HOST_HEADER");
}
if (!saleorApiUrl) {
this.debug("Missing saleor-api-url header");
throw new WebhookError("Missing saleor-api-url header", "MISSING_API_URL_HEADER");
}
if (!event) {
this.debug("Missing saleor-event header");
throw new WebhookError("Missing saleor-event header", "MISSING_EVENT_HEADER");
}
const expected = allowedEvent.toLowerCase();
if (event !== expected) {
this.debug(`Wrong incoming request event: ${event}. Expected: ${expected}`);
throw new WebhookError(
`Wrong incoming request event: ${event}. Expected: ${expected}`,
"WRONG_EVENT",
);
}
if (!signature) {
this.debug("No signature");
throw new WebhookError("Missing saleor-signature header", "MISSING_SIGNATURE_HEADER");
}
const rawBody = await adapter.getRawBody();
if (!rawBody) {
this.debug("Missing request body");
throw new WebhookError("Missing request body", "MISSING_REQUEST_BODY");
}
let parsedBody: unknown & { version?: string | null };
try {
parsedBody = JSON.parse(rawBody);
} catch {
this.debug("Request body cannot be parsed");
throw new WebhookError("Request body can't be parsed", "CANT_BE_PARSED");
}
/**
* Can be undefined - subscription must contain "version", otherwise nothing to parse
*/
let parsedSchemaVersion: SaleorSchemaVersion | null = null;
try {
parsedSchemaVersion = parseSchemaVersion(parsedBody.version);
} catch {
this.debug("Schema version cannot be parsed");
}
/**
* Verify if the app is properly installed for given Saleor API URL
*/
const authData = await apl.get(saleorApiUrl);
if (!authData) {
this.debug("APL didn't found auth data for %s", saleorApiUrl);
throw new WebhookError(
`Can't find auth data for ${saleorApiUrl}. Please register the application`,
"NOT_REGISTERED",
);
}
/**
* Verify payload signature
*/
try {
this.debug("Will verify signature with JWKS saved in AuthData");
if (!authData.jwks) {
throw new Error("JWKS not found in AuthData");
}
await verifySignatureWithJwks(authData.jwks, signature, rawBody);
} catch {
this.debug("Request signature check failed. Refresh the JWKS cache and check again");
const newJwks = await fetchRemoteJwks(authData.saleorApiUrl).catch((e) => {
this.debug(e);
throw new WebhookError(
"Fetching remote JWKS failed",
"SIGNATURE_VERIFICATION_FAILED",
);
});
this.debug("Fetched refreshed JWKS");
try {
this.debug(
"Second attempt to validate the signature JWKS, using fresh tokens from the API",
);
await verifySignatureWithJwks(newJwks, signature, rawBody);
this.debug("Verification successful - update JWKS in the AuthData");
await apl.set({ ...authData, jwks: newJwks });
} catch {
this.debug("Second attempt also ended with validation error. Reject the webhook");
throw new WebhookError(
"Request signature check failed",
"SIGNATURE_VERIFICATION_FAILED",
);
}
}
span.setStatus({
code: SpanStatusCode.OK,
});
return {
baseUrl,
event,
payload: parsedBody as TPayload,
authData,
schemaVersion: parsedSchemaVersion,
};
} catch (err) {
const message = (err as Error)?.message ?? "Unknown error";
span.setStatus({
code: SpanStatusCode.ERROR,
message,
});
throw err;
} finally {
span.end();
}
},
);
}
}