-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b0a237a
commit 9e5c059
Showing
12 changed files
with
494 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
src/handlers/fetch-api/saleor-webhooks/process-saleor-webhook.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import { APL } from "../../../APL"; | ||
import { createDebug } from "../../../debug"; | ||
import { fetchRemoteJwks } from "../../../fetch-remote-jwks"; | ||
import { getBaseUrlFetchAPI, getSaleorHeadersFetchAPI } from "../../../headers"; | ||
import { parseSchemaVersion } from "../../../util"; | ||
import { verifySignatureWithJwks } from "../../../verify-signature"; | ||
import { WebhookContext, WebhookError } from "../../shared/process-saleor-webhook"; | ||
|
||
const debug = createDebug("WebAPI:processSaleorWebhook"); | ||
|
||
interface ProcessSaleorWebhookArgs { | ||
req: Request; | ||
apl: APL; | ||
allowedEvent: string; | ||
} | ||
|
||
export const processSaleorWebhook = async <T>({ | ||
req, | ||
apl, | ||
allowedEvent, | ||
}: ProcessSaleorWebhookArgs): Promise<WebhookContext<T>> => { | ||
// TODO: Add OTEL | ||
|
||
try { | ||
debug("Request processing started"); | ||
|
||
if (req.method !== "POST") { | ||
debug("Wrong HTTP method"); | ||
throw new WebhookError("Wrong request method, only POST allowed", "WRONG_METHOD"); | ||
} | ||
|
||
const { event, signature, saleorApiUrl } = getSaleorHeadersFetchAPI(req.headers); | ||
const baseUrl = getBaseUrlFetchAPI(req.headers); | ||
|
||
if (!baseUrl) { | ||
debug("Missing host header"); | ||
throw new WebhookError("Missing host header", "MISSING_HOST_HEADER"); | ||
} | ||
|
||
if (!saleorApiUrl) { | ||
debug("Missing saleor-api-url header"); | ||
throw new WebhookError("Missing saleor-api-url header", "MISSING_API_URL_HEADER"); | ||
} | ||
|
||
if (!event) { | ||
debug("Missing saleor-event header"); | ||
throw new WebhookError("Missing saleor-event header", "MISSING_EVENT_HEADER"); | ||
} | ||
|
||
const expected = allowedEvent.toLowerCase(); | ||
|
||
if (event !== expected) { | ||
debug(`Wrong incoming request event: ${event}. Expected: ${expected}`); | ||
|
||
throw new WebhookError( | ||
`Wrong incoming request event: ${event}. Expected: ${expected}`, | ||
"WRONG_EVENT" | ||
); | ||
} | ||
|
||
if (!signature) { | ||
debug("No signature"); | ||
|
||
throw new WebhookError("Missing saleor-signature header", "MISSING_SIGNATURE_HEADER"); | ||
} | ||
|
||
let rawBody: string; | ||
|
||
try { | ||
rawBody = await req.text(); | ||
} catch (err) { | ||
throw new WebhookError("Error reading request body", "CANT_BE_PARSED"); | ||
} | ||
|
||
if (!rawBody) { | ||
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 (err) { | ||
throw new WebhookError("Request body can't be parsed", "CANT_BE_PARSED"); | ||
} | ||
|
||
let parsedSchemaVersion: number | null = null; | ||
|
||
try { | ||
parsedSchemaVersion = parseSchemaVersion(parsedBody.version); | ||
} catch { | ||
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) { | ||
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 | ||
* | ||
* TODO: Add test for repeat verification scenario | ||
*/ | ||
try { | ||
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 { | ||
debug("Request signature check failed. Refresh the JWKS cache and check again"); | ||
|
||
const newJwks = await fetchRemoteJwks(authData.saleorApiUrl).catch((e) => { | ||
debug(e); | ||
|
||
throw new WebhookError("Fetching remote JWKS failed", "SIGNATURE_VERIFICATION_FAILED"); | ||
}); | ||
|
||
debug("Fetched refreshed JWKS"); | ||
|
||
try { | ||
debug("Second attempt to validate the signature JWKS, using fresh tokens from the API"); | ||
|
||
await verifySignatureWithJwks(newJwks, signature, rawBody); | ||
|
||
debug("Verification successful - update JWKS in the AuthData"); | ||
|
||
await apl.set({ ...authData, jwks: newJwks }); | ||
} catch { | ||
debug("Second attempt also ended with validation error. Reject the webhook"); | ||
|
||
throw new WebhookError("Request signature check failed", "SIGNATURE_VERIFICATION_FAILED"); | ||
} | ||
} | ||
|
||
return { | ||
baseUrl, | ||
event, | ||
payload: parsedBody as T, | ||
authData, | ||
schemaVersion: parsedSchemaVersion, | ||
}; | ||
} catch (err) { | ||
debug("Unexpected error: %O", err); | ||
|
||
throw err; | ||
} | ||
}; |
48 changes: 48 additions & 0 deletions
48
src/handlers/fetch-api/saleor-webhooks/saleor-async-webhook.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { ASTNode } from "graphql"; | ||
|
||
import { AsyncWebhookEventType } from "../../../types"; | ||
import { SaleorWebApiWebhook, WebApiWebhookHandler, WebhookConfig } from "./saleor-webhook"; | ||
|
||
export class SaleorAsyncWebhook<TPayload = unknown> extends SaleorWebApiWebhook<TPayload> { | ||
readonly event: AsyncWebhookEventType; | ||
|
||
protected readonly eventType = "async" as const; | ||
|
||
constructor( | ||
/** | ||
* Omit new required fields and make them optional. Validate in constructor. | ||
* In 0.35.0 remove old fields | ||
*/ | ||
configuration: Omit<WebhookConfig<AsyncWebhookEventType>, "event" | "query"> & { | ||
/** | ||
* @deprecated - use `event` instead. Will be removed in 0.35.0 | ||
*/ | ||
asyncEvent?: AsyncWebhookEventType; | ||
event?: AsyncWebhookEventType; | ||
query?: string | ASTNode; | ||
} | ||
) { | ||
if (!configuration.event && !configuration.asyncEvent) { | ||
throw new Error("event or asyncEvent must be provided. asyncEvent is deprecated"); | ||
} | ||
|
||
if (!configuration.query && !configuration.subscriptionQueryAst) { | ||
throw new Error( | ||
"query or subscriptionQueryAst must be provided. subscriptionQueryAst is deprecated" | ||
); | ||
} | ||
|
||
super({ | ||
...configuration, | ||
event: configuration.event! ?? configuration.asyncEvent!, | ||
query: configuration.query! ?? configuration.subscriptionQueryAst!, | ||
}); | ||
|
||
this.event = configuration.event! ?? configuration.asyncEvent!; | ||
this.query = configuration.query! ?? configuration.subscriptionQueryAst!; | ||
} | ||
|
||
createHandler(handlerFn: WebApiWebhookHandler<TPayload>): WebApiWebhookHandler { | ||
return super.createHandler(handlerFn); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
src/handlers/fetch-api/saleor-webhooks/saleor-sync-webhook.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { SyncWebhookEventType } from "../../../types"; | ||
import { buildSyncWebhookResponsePayload } from "../../shared/sync-webhook-response-builder"; | ||
import { SaleorWebApiWebhook, WebApiWebhookHandler, WebhookConfig } from "./saleor-webhook"; | ||
|
||
type InjectedContext<TEvent extends SyncWebhookEventType> = { | ||
buildResponse: typeof buildSyncWebhookResponsePayload<TEvent>; | ||
}; | ||
export class SaleorSyncWebhook< | ||
TPayload = unknown, | ||
TEvent extends SyncWebhookEventType = SyncWebhookEventType | ||
> extends SaleorWebApiWebhook<TPayload, InjectedContext<TEvent>> { | ||
readonly event: TEvent; | ||
|
||
protected readonly eventType = "sync" as const; | ||
|
||
protected extraContext = { | ||
buildResponse: buildSyncWebhookResponsePayload, | ||
}; | ||
|
||
constructor(configuration: WebhookConfig<TEvent>) { | ||
super(configuration); | ||
|
||
this.event = configuration.event; | ||
} | ||
|
||
createHandler( | ||
handlerFn: WebApiWebhookHandler< | ||
TPayload, | ||
{ | ||
buildResponse: typeof buildSyncWebhookResponsePayload<TEvent>; | ||
} | ||
> | ||
): WebApiWebhookHandler { | ||
return super.createHandler(handlerFn); | ||
} | ||
} |
Oops, something went wrong.