Add saleorApiUrl state param to AppBridge (#114)
* Add saleorApiUrl state param to AppBridge * Apply payload verification check based on full api endpoint url * Remove deprecation info * Check if either domain or saleorApiUrl is provided
This commit is contained in:
parent
6fbf692a44
commit
ddd65dabf1
14 changed files with 128 additions and 26 deletions
|
@ -17,6 +17,8 @@ Options object is following:
|
||||||
```
|
```
|
||||||
type AppBridgeOptions = {
|
type AppBridgeOptions = {
|
||||||
targetDomain?: string;
|
targetDomain?: string;
|
||||||
|
saleorApiUrl?: string;
|
||||||
|
initialLocale?: LocaleCode;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -36,6 +38,11 @@ type AppBridgeState = {
|
||||||
domain: string;
|
domain: string;
|
||||||
path: string;
|
path: string;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
|
locale: LocaleCode; // See src/locales.ts
|
||||||
|
/**
|
||||||
|
* Full URL including protocol and path where graphql is available
|
||||||
|
*/
|
||||||
|
saleorApiUrl: string;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { AppBridge } from "./app-bridge";
|
import { AppBridge } from "./app-bridge";
|
||||||
import { AppBridgeProvider, useAppBridge } from "./app-bridge-provider";
|
import { AppBridgeProvider, useAppBridge } from "./app-bridge-provider";
|
||||||
|
import { AppIframeParams } from "./app-iframe-params";
|
||||||
import { DashboardEventFactory } from "./events";
|
import { DashboardEventFactory } from "./events";
|
||||||
|
|
||||||
const origin = "http://example.com";
|
const origin = "http://example.com";
|
||||||
const domain = "saleor.domain.host";
|
const domain = "saleor.domain.host";
|
||||||
|
const apiUrl = "https://saleor.domain.host/graphql/";
|
||||||
|
|
||||||
Object.defineProperty(window.document, "referrer", {
|
Object.defineProperty(window.document, "referrer", {
|
||||||
value: origin,
|
value: origin,
|
||||||
|
@ -16,7 +18,7 @@ Object.defineProperty(window.document, "referrer", {
|
||||||
|
|
||||||
Object.defineProperty(window, "location", {
|
Object.defineProperty(window, "location", {
|
||||||
value: {
|
value: {
|
||||||
href: `${origin}?domain=${domain}&id=appid`,
|
href: `${origin}?${AppIframeParams.DOMAIN}=${domain}&${AppIframeParams.APP_ID}=appid&${AppIframeParams.SALEOR_API_URL}=${apiUrl}`,
|
||||||
},
|
},
|
||||||
writable: true,
|
writable: true,
|
||||||
});
|
});
|
||||||
|
@ -37,7 +39,8 @@ describe("AppBridgeProvider", () => {
|
||||||
<AppBridgeProvider
|
<AppBridgeProvider
|
||||||
appBridgeInstance={
|
appBridgeInstance={
|
||||||
new AppBridge({
|
new AppBridge({
|
||||||
targetDomain: "https://test-domain",
|
targetDomain: domain,
|
||||||
|
saleorApiUrl: apiUrl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -86,19 +89,20 @@ describe("useAppBridge hook", () => {
|
||||||
|
|
||||||
it("Returned instance provided in Provider", () => {
|
it("Returned instance provided in Provider", () => {
|
||||||
const appBridge = new AppBridge({
|
const appBridge = new AppBridge({
|
||||||
targetDomain: "test-domain",
|
targetDomain: domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() => useAppBridge(), {
|
const { result } = renderHook(() => useAppBridge(), {
|
||||||
wrapper: (props: {}) => <AppBridgeProvider {...props} appBridgeInstance={appBridge} />,
|
wrapper: (props: {}) => <AppBridgeProvider {...props} appBridgeInstance={appBridge} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.appBridge?.getState().domain).toBe("test-domain");
|
expect(result.current.appBridge?.getState().domain).toBe(domain);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Stores active state in React State", () => {
|
it("Stores active state in React State", () => {
|
||||||
const appBridge = new AppBridge({
|
const appBridge = new AppBridge({
|
||||||
targetDomain: origin,
|
targetDomain: domain,
|
||||||
|
saleorApiUrl: apiUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderCallback = vi.fn();
|
const renderCallback = vi.fn();
|
||||||
|
@ -128,12 +132,13 @@ describe("useAppBridge hook", () => {
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
expect(renderCallback).toHaveBeenCalledTimes(2);
|
expect(renderCallback).toHaveBeenCalledTimes(2);
|
||||||
expect(renderCallback).toHaveBeenCalledWith({
|
expect(renderCallback).toHaveBeenCalledWith({
|
||||||
domain: "http://example.com",
|
domain,
|
||||||
id: "appid",
|
id: "appid",
|
||||||
path: "",
|
path: "",
|
||||||
ready: false,
|
ready: false,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
locale: "en",
|
locale: "en",
|
||||||
|
saleorApiUrl: apiUrl,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,7 @@ describe("app-bridge-state.ts", () => {
|
||||||
path: "/",
|
path: "/",
|
||||||
theme: "light",
|
theme: "light",
|
||||||
locale: "en",
|
locale: "en",
|
||||||
|
saleorApiUrl: "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -20,7 +21,8 @@ describe("app-bridge-state.ts", () => {
|
||||||
const instance = new AppBridgeStateContainer();
|
const instance = new AppBridgeStateContainer();
|
||||||
|
|
||||||
const newState: Partial<AppBridgeState> = {
|
const newState: Partial<AppBridgeState> = {
|
||||||
domain: "https://my-saleor-instance.cloud",
|
domain: "my-saleor-instance.cloud",
|
||||||
|
saleorApiUrl: "https://my-saleor-instance.cloud/graphql/",
|
||||||
id: "foo-bar",
|
id: "foo-bar",
|
||||||
path: "/",
|
path: "/",
|
||||||
theme: "light",
|
theme: "light",
|
||||||
|
|
|
@ -9,6 +9,7 @@ export type AppBridgeState = {
|
||||||
path: string;
|
path: string;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
locale: LocaleCode;
|
locale: LocaleCode;
|
||||||
|
saleorApiUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
|
@ -19,6 +20,7 @@ export class AppBridgeStateContainer {
|
||||||
private state: AppBridgeState = {
|
private state: AppBridgeState = {
|
||||||
id: "",
|
id: "",
|
||||||
domain: "",
|
domain: "",
|
||||||
|
saleorApiUrl: "",
|
||||||
ready: false,
|
ready: false,
|
||||||
path: "/",
|
path: "/",
|
||||||
theme: "light",
|
theme: "light",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import debugPkg from "debug";
|
||||||
import { LocaleCode } from "../locales";
|
import { LocaleCode } from "../locales";
|
||||||
import { Actions } from "./actions";
|
import { Actions } from "./actions";
|
||||||
import { AppBridgeState, AppBridgeStateContainer } from "./app-bridge-state";
|
import { AppBridgeState, AppBridgeStateContainer } from "./app-bridge-state";
|
||||||
|
import { AppIframeParams } from "./app-iframe-params";
|
||||||
import { SSR } from "./constants";
|
import { SSR } from "./constants";
|
||||||
import { Events, EventType, PayloadOfEvent, ThemeType } from "./events";
|
import { Events, EventType, PayloadOfEvent, ThemeType } from "./events";
|
||||||
|
|
||||||
|
@ -68,6 +69,7 @@ const createEmptySubscribeMap = (): SubscribeMap => ({
|
||||||
|
|
||||||
export type AppBridgeOptions = {
|
export type AppBridgeOptions = {
|
||||||
targetDomain?: string;
|
targetDomain?: string;
|
||||||
|
saleorApiUrl?: string;
|
||||||
initialLocale?: LocaleCode;
|
initialLocale?: LocaleCode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -75,16 +77,22 @@ export type AppBridgeOptions = {
|
||||||
* TODO: Consider validating locale if wrong code provided
|
* TODO: Consider validating locale if wrong code provided
|
||||||
*/
|
*/
|
||||||
const getLocaleFromUrl = () =>
|
const getLocaleFromUrl = () =>
|
||||||
(new URL(window.location.href).searchParams.get("locale") as LocaleCode) || undefined;
|
(new URL(window.location.href).searchParams.get(AppIframeParams.LOCALE) as LocaleCode) ||
|
||||||
|
undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Probably remove empty string fallback
|
* TODO: Probably remove empty string fallback
|
||||||
*/
|
*/
|
||||||
const getDomainFromUrl = () => new URL(window.location.href).searchParams.get("domain") || "";
|
const getDomainFromUrl = () =>
|
||||||
|
new URL(window.location.href).searchParams.get(AppIframeParams.DOMAIN) || "";
|
||||||
|
|
||||||
|
const getSaleorApiUrlFromUrl = () =>
|
||||||
|
new URL(window.location.href).searchParams.get(AppIframeParams.SALEOR_API_URL) || "";
|
||||||
|
|
||||||
const getDefaultOptions = (): AppBridgeOptions => ({
|
const getDefaultOptions = (): AppBridgeOptions => ({
|
||||||
targetDomain: getDomainFromUrl(),
|
targetDomain: getDomainFromUrl(),
|
||||||
initialLocale: getLocaleFromUrl(),
|
saleorApiUrl: getSaleorApiUrlFromUrl(),
|
||||||
|
initialLocale: getLocaleFromUrl() ?? "en",
|
||||||
});
|
});
|
||||||
|
|
||||||
export class AppBridge {
|
export class AppBridge {
|
||||||
|
@ -121,9 +129,17 @@ export class AppBridge {
|
||||||
console.warn("document.referrer is empty");
|
console.warn("document.referrer is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.combinedOptions.saleorApiUrl) {
|
||||||
|
debug("?saleorApiUrl was not found in iframe url");
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.combinedOptions.targetDomain) {
|
if (!this.combinedOptions.targetDomain) {
|
||||||
|
debug("?domain was not found in iframe url");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(this.combinedOptions.saleorApiUrl && this.combinedOptions.targetDomain)) {
|
||||||
console.error(
|
console.error(
|
||||||
"No domain set, ensure ?domain param in iframe exist or provide in AppBridge constructor"
|
"domain and saleorApiUrl params were not found in iframe url. Ensure at least one of them is present"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,11 +259,19 @@ export class AppBridge {
|
||||||
debug("setInitialState() called");
|
debug("setInitialState() called");
|
||||||
|
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const id = url.searchParams.get("id") || "";
|
const id = url.searchParams.get(AppIframeParams.APP_ID) || "";
|
||||||
const path = window.location.pathname || "";
|
const path = window.location.pathname || "";
|
||||||
const theme: ThemeType = url.searchParams.get("theme") === "light" ? "light" : "dark";
|
const theme: ThemeType =
|
||||||
|
url.searchParams.get(AppIframeParams.THEME) === "light" ? "light" : "dark";
|
||||||
|
|
||||||
const state = { domain: this.combinedOptions.targetDomain, id, path, theme };
|
const state: Partial<AppBridgeState> = {
|
||||||
|
domain: this.combinedOptions.targetDomain,
|
||||||
|
id,
|
||||||
|
path,
|
||||||
|
theme,
|
||||||
|
saleorApiUrl: this.combinedOptions.saleorApiUrl,
|
||||||
|
locale: this.combinedOptions.initialLocale,
|
||||||
|
};
|
||||||
|
|
||||||
debug("setInitialState() will setState with %j", state);
|
debug("setInitialState() will setState with %j", state);
|
||||||
|
|
||||||
|
|
10
src/app-bridge/app-iframe-params.ts
Normal file
10
src/app-bridge/app-iframe-params.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* Contains keys of SearchParams added to iframe src
|
||||||
|
*/
|
||||||
|
export const AppIframeParams = {
|
||||||
|
APP_ID: "id",
|
||||||
|
THEME: "theme",
|
||||||
|
DOMAIN: "domain",
|
||||||
|
SALEOR_API_URL: "saleorApiUrl",
|
||||||
|
LOCALE: "locale",
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ export { AppBridge };
|
||||||
|
|
||||||
export * from "./actions";
|
export * from "./actions";
|
||||||
export * from "./app-bridge-provider";
|
export * from "./app-bridge-provider";
|
||||||
|
export * from "./app-iframe-params";
|
||||||
export * from "./events";
|
export * from "./events";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
export * from "./use-dashboard-token";
|
export * from "./use-dashboard-token";
|
||||||
|
|
|
@ -2,5 +2,6 @@ export const SALEOR_DOMAIN_HEADER = "saleor-domain";
|
||||||
export const SALEOR_EVENT_HEADER = "saleor-event";
|
export const SALEOR_EVENT_HEADER = "saleor-event";
|
||||||
export const SALEOR_SIGNATURE_HEADER = "saleor-signature";
|
export const SALEOR_SIGNATURE_HEADER = "saleor-signature";
|
||||||
export const SALEOR_AUTHORIZATION_BEARER_HEADER = "authorization-bearer";
|
export const SALEOR_AUTHORIZATION_BEARER_HEADER = "authorization-bearer";
|
||||||
|
export const SALEOR_API_URL_HEADER = "saleor-api-url";
|
||||||
|
|
||||||
export * from "./locales";
|
export * from "./locales";
|
||||||
|
|
|
@ -13,6 +13,11 @@ vi.mock("./../../verify-signature", () => ({
|
||||||
throw new Error("Wrong signature");
|
throw new Error("Wrong signature");
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
verifySignatureFromApiUrl: vi.fn((domain, signature) => {
|
||||||
|
if (signature !== "mocked_signature") {
|
||||||
|
throw new Error("Wrong signature");
|
||||||
|
}
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("raw-body", () => ({
|
vi.mock("raw-body", () => ({
|
||||||
|
@ -43,6 +48,7 @@ describe("processAsyncSaleorWebhook", () => {
|
||||||
host: "some-saleor-host.cloud",
|
host: "some-saleor-host.cloud",
|
||||||
"x-forwarded-proto": "https",
|
"x-forwarded-proto": "https",
|
||||||
"saleor-domain": "example.com",
|
"saleor-domain": "example.com",
|
||||||
|
"saleor-api-url": "https://example.com/graphql/",
|
||||||
"saleor-event": "product_updated",
|
"saleor-event": "product_updated",
|
||||||
"saleor-signature": "mocked_signature",
|
"saleor-signature": "mocked_signature",
|
||||||
"content-length": "0", // is ignored by mocked raw-body
|
"content-length": "0", // is ignored by mocked raw-body
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { APL } from "../../APL";
|
||||||
import { AuthData } from "../../APL/apl";
|
import { AuthData } from "../../APL/apl";
|
||||||
import { createDebug } from "../../debug";
|
import { createDebug } from "../../debug";
|
||||||
import { getBaseUrl, getSaleorHeaders } from "../../headers";
|
import { getBaseUrl, getSaleorHeaders } from "../../headers";
|
||||||
import { verifySignature } from "../../verify-signature";
|
import { verifySignature, verifySignatureFromApiUrl } from "../../verify-signature";
|
||||||
|
|
||||||
const debug = createDebug("processAsyncWebhook");
|
const debug = createDebug("processAsyncWebhook");
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export const processAsyncSaleorWebhook: ProcessAsyncSaleorWebhook = async <T>({
|
||||||
debug("Wrong HTTP method");
|
debug("Wrong HTTP method");
|
||||||
throw new WebhookError("Wrong request method, only POST allowed", "WRONG_METHOD");
|
throw new WebhookError("Wrong request method, only POST allowed", "WRONG_METHOD");
|
||||||
}
|
}
|
||||||
const { event, domain, signature } = getSaleorHeaders(req.headers);
|
const { event, domain, signature, saleorApiUrl } = getSaleorHeaders(req.headers);
|
||||||
const baseUrl = getBaseUrl(req.headers);
|
const baseUrl = getBaseUrl(req.headers);
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
|
@ -134,7 +134,14 @@ export const processAsyncSaleorWebhook: ProcessAsyncSaleorWebhook = async <T>({
|
||||||
// Payload signature check
|
// Payload signature check
|
||||||
// TODO: Since it require additional request, can we cache it's response?
|
// TODO: Since it require additional request, can we cache it's response?
|
||||||
try {
|
try {
|
||||||
|
/**
|
||||||
|
* saleorApiUrl is a new header, is it if available. Verification by domain will be removed in future versions
|
||||||
|
*/
|
||||||
|
if (saleorApiUrl) {
|
||||||
|
await verifySignatureFromApiUrl(saleorApiUrl, signature, rawBody);
|
||||||
|
} else {
|
||||||
await verifySignature(domain, signature, rawBody);
|
await verifySignature(domain, signature, rawBody);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
debug("Request signature check failed");
|
debug("Request signature check failed");
|
||||||
throw new WebhookError("Request signature check failed", "SIGNATURE_VERIFICATION_FAILED");
|
throw new WebhookError("Request signature check failed", "SIGNATURE_VERIFICATION_FAILED");
|
||||||
|
|
|
@ -127,9 +127,6 @@ export class SaleorAsyncWebhook<TPayload = unknown> {
|
||||||
/**
|
/**
|
||||||
* Wraps provided function, to ensure incoming request comes from registered Saleor instance.
|
* Wraps provided function, to ensure incoming request comes from registered Saleor instance.
|
||||||
* Also provides additional `context` object containing typed payload and request properties.
|
* Also provides additional `context` object containing typed payload and request properties.
|
||||||
*
|
|
||||||
* @param handlerFn NextApiHandler function which takes additional `context` argument
|
|
||||||
* @returns NextApiHandler
|
|
||||||
*/
|
*/
|
||||||
createHandler(handlerFn: NextWebhookApiHandler<TPayload>): NextApiHandler {
|
createHandler(handlerFn: NextWebhookApiHandler<TPayload>): NextApiHandler {
|
||||||
return async (req, res) => {
|
return async (req, res) => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
SALEOR_API_URL_HEADER,
|
||||||
SALEOR_AUTHORIZATION_BEARER_HEADER,
|
SALEOR_AUTHORIZATION_BEARER_HEADER,
|
||||||
SALEOR_DOMAIN_HEADER,
|
SALEOR_DOMAIN_HEADER,
|
||||||
SALEOR_EVENT_HEADER,
|
SALEOR_EVENT_HEADER,
|
||||||
|
@ -8,13 +9,12 @@ import {
|
||||||
const toStringOrUndefined = (value: string | string[] | undefined) =>
|
const toStringOrUndefined = (value: string | string[] | undefined) =>
|
||||||
value ? value.toString() : undefined;
|
value ? value.toString() : undefined;
|
||||||
|
|
||||||
export const getSaleorHeaders = (headers: {
|
export const getSaleorHeaders = (headers: { [name: string]: string | string[] | undefined }) => ({
|
||||||
[name: string]: string | string[] | undefined;
|
|
||||||
}): Record<string, string | undefined> => ({
|
|
||||||
domain: toStringOrUndefined(headers[SALEOR_DOMAIN_HEADER]),
|
domain: toStringOrUndefined(headers[SALEOR_DOMAIN_HEADER]),
|
||||||
authorizationBearer: toStringOrUndefined(headers[SALEOR_AUTHORIZATION_BEARER_HEADER]),
|
authorizationBearer: toStringOrUndefined(headers[SALEOR_AUTHORIZATION_BEARER_HEADER]),
|
||||||
signature: toStringOrUndefined(headers[SALEOR_SIGNATURE_HEADER]),
|
signature: toStringOrUndefined(headers[SALEOR_SIGNATURE_HEADER]),
|
||||||
event: toStringOrUndefined(headers[SALEOR_EVENT_HEADER]),
|
event: toStringOrUndefined(headers[SALEOR_EVENT_HEADER]),
|
||||||
|
saleorApiUrl: toStringOrUndefined(headers[SALEOR_API_URL_HEADER]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
|
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
|
||||||
|
|
|
@ -4,9 +4,18 @@
|
||||||
const resolveUrlProtocol = (saleorDomain: string): string =>
|
const resolveUrlProtocol = (saleorDomain: string): string =>
|
||||||
saleorDomain === "localhost:8000" ? "http" : "https";
|
saleorDomain === "localhost:8000" ? "http" : "https";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use getJwksUrlFromSaleorApiUrl
|
||||||
|
*/
|
||||||
export const getJwksUrl = (saleorDomain: string): string =>
|
export const getJwksUrl = (saleorDomain: string): string =>
|
||||||
`${resolveUrlProtocol(saleorDomain)}://${saleorDomain}/.well-known/jwks.json`;
|
`${resolveUrlProtocol(saleorDomain)}://${saleorDomain}/.well-known/jwks.json`;
|
||||||
|
|
||||||
|
export const getJwksUrlFromSaleorApiUrl = (saleorApiUrl: string): string =>
|
||||||
|
`${new URL(saleorApiUrl).origin}/.well-known/jwks.json`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use saleor-api-url header
|
||||||
|
*/
|
||||||
export const getGraphQLUrl = (saleorDomain: string): string =>
|
export const getGraphQLUrl = (saleorDomain: string): string =>
|
||||||
`${resolveUrlProtocol(saleorDomain)}://${saleorDomain}/graphql/`;
|
`${resolveUrlProtocol(saleorDomain)}://${saleorDomain}/graphql/`;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import * as jose from "jose";
|
import * as jose from "jose";
|
||||||
|
|
||||||
import { createDebug } from "./debug";
|
import { createDebug } from "./debug";
|
||||||
import { getJwksUrl } from "./urls";
|
import { getJwksUrl, getJwksUrlFromSaleorApiUrl } from "./urls";
|
||||||
|
|
||||||
const debug = createDebug("verify-signature");
|
const debug = createDebug("verify-signature");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify payload signature with public key of given `domain`
|
* @deprecated
|
||||||
* https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks#payload-signature
|
* use verifySignatureFromApiUrl
|
||||||
*/
|
*/
|
||||||
export const verifySignature = async (domain: string, signature: string, rawBody: string) => {
|
export const verifySignature = async (domain: string, signature: string, rawBody: string) => {
|
||||||
const [header, , jwsSignature] = signature.split(".");
|
const [header, , jwsSignature] = signature.split(".");
|
||||||
|
@ -31,3 +31,34 @@ export const verifySignature = async (domain: string, signature: string, rawBody
|
||||||
throw new Error("JWKS verification failed");
|
throw new Error("JWKS verification failed");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify payload signature with public key of given `domain`
|
||||||
|
* https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks#payload-signature
|
||||||
|
*/
|
||||||
|
export const verifySignatureFromApiUrl = async (
|
||||||
|
apiUrl: string,
|
||||||
|
signature: string,
|
||||||
|
rawBody: string
|
||||||
|
) => {
|
||||||
|
const [header, , jwsSignature] = signature.split(".");
|
||||||
|
const jws: jose.FlattenedJWSInput = {
|
||||||
|
protected: header,
|
||||||
|
payload: rawBody,
|
||||||
|
signature: jwsSignature,
|
||||||
|
};
|
||||||
|
|
||||||
|
const remoteJwks = jose.createRemoteJWKSet(
|
||||||
|
new URL(getJwksUrlFromSaleorApiUrl(apiUrl))
|
||||||
|
) as jose.FlattenedVerifyGetKey;
|
||||||
|
|
||||||
|
debug("Created remote JWKS");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await jose.flattenedVerify(jws, remoteJwks);
|
||||||
|
debug("JWKS verified");
|
||||||
|
} catch {
|
||||||
|
debug("JWKS verification failed");
|
||||||
|
throw new Error("JWKS verification failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue