diff --git a/package.json b/package.json index 470a7e9..04050a7 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,11 @@ "types": "./verify-jwt.d.ts", "import": "./verify-jwt.mjs", "require": "./verify-jwt.js" + }, + "./verify-signature": { + "types": "./verify-signature.d.ts", + "import": "./verify-signature.mjs", + "require": "./verify-signature.js" } }, "publishConfig": { diff --git a/src/middleware/with-jwt-verified.ts b/src/middleware/with-jwt-verified.ts index e403146..16ff6e9 100644 --- a/src/middleware/with-jwt-verified.ts +++ b/src/middleware/with-jwt-verified.ts @@ -2,9 +2,9 @@ import * as jose from "jose"; import type { Middleware, Request } from "retes"; import { Response } from "retes/response"; -import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_DOMAIN_HEADER } from "../const"; +import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "../const"; import { getSaleorHeaders } from "../headers"; -import { getJwksUrl } from "../urls"; +import { getJwksUrlFromSaleorApiUrl } from "../urls"; import { createMiddlewareDebug } from "./middleware-debug"; const debug = createMiddlewareDebug("withJWTVerified"); @@ -19,9 +19,9 @@ export const withJWTVerified = (getAppId: (request: Request) => Promise): Middleware => (handler) => async (request) => { - const { domain, authorizationBearer: token } = getSaleorHeaders(request.headers); + const { authorizationBearer: token, saleorApiUrl } = getSaleorHeaders(request.headers); - debug("Middleware called with domain: \"%s\"", domain); + debug("Middleware called with apiUrl: \"%s\"", saleorApiUrl); if (typeof token !== "string") { debug("Middleware with empty token, will response with Bad Request", token); @@ -34,10 +34,10 @@ export const withJWTVerified = debug("Middleware called with token starting with: \"%s\"", token.substring(0, 4)); - if (domain === undefined) { + if (saleorApiUrl === undefined) { return Response.BadRequest({ success: false, - message: `${ERROR_MESSAGE} Missing ${SALEOR_DOMAIN_HEADER} header.`, + message: `${ERROR_MESSAGE} Missing ${SALEOR_API_URL_HEADER} header.`, }); } @@ -93,7 +93,7 @@ export const withJWTVerified = try { debug("Trying to create JWKS"); - const JWKS = jose.createRemoteJWKSet(new URL(getJwksUrl(domain))); + const JWKS = jose.createRemoteJWKSet(new URL(getJwksUrlFromSaleorApiUrl(saleorApiUrl))); debug("Trying to compare JWKS with token"); await jose.jwtVerify(token, JWKS); } catch (e) { diff --git a/src/middleware/with-webhook-signature-verified.ts b/src/middleware/with-webhook-signature-verified.ts index 4be9197..516eea6 100644 --- a/src/middleware/with-webhook-signature-verified.ts +++ b/src/middleware/with-webhook-signature-verified.ts @@ -2,15 +2,18 @@ import crypto from "crypto"; import { Middleware } from "retes"; import { Response } from "retes/response"; -import { SALEOR_DOMAIN_HEADER, SALEOR_SIGNATURE_HEADER } from "../const"; +import { SALEOR_API_URL_HEADER, SALEOR_SIGNATURE_HEADER } from "../const"; import { getSaleorHeaders } from "../headers"; -import { verifySignature } from "../verify-signature"; +import { verifySignatureFromApiUrl } from "../verify-signature"; import { createMiddlewareDebug } from "./middleware-debug"; const debug = createMiddlewareDebug("withWebhookSignatureVerified"); const ERROR_MESSAGE = "Webhook signature verification failed:"; +/** + * TODO: Add test + */ export const withWebhookSignatureVerified = (secretKey: string | undefined = undefined): Middleware => (handler) => @@ -26,7 +29,7 @@ export const withWebhookSignatureVerified = }); } - const { domain: saleorDomain, signature: payloadSignature } = getSaleorHeaders(request.headers); + const { signature: payloadSignature, saleorApiUrl } = getSaleorHeaders(request.headers); if (!payloadSignature) { debug("Signature header was not found"); @@ -37,10 +40,10 @@ export const withWebhookSignatureVerified = }); } - if (!saleorDomain) { + if (!saleorApiUrl) { return Response.BadRequest({ success: false, - message: `${ERROR_MESSAGE} Missing ${SALEOR_DOMAIN_HEADER} header.`, + message: `${ERROR_MESSAGE} Missing ${SALEOR_API_URL_HEADER} header.`, }); } @@ -62,7 +65,7 @@ export const withWebhookSignatureVerified = } } else { try { - await verifySignature(saleorDomain, payloadSignature, request.rawBody); + await verifySignatureFromApiUrl(saleorApiUrl, payloadSignature, request.rawBody); debug("JWKS verified"); } catch { debug("JWKS verification failed"); diff --git a/src/urls.test.ts b/src/urls.test.ts index be79d6a..5ecd690 100644 --- a/src/urls.test.ts +++ b/src/urls.test.ts @@ -1,26 +1,13 @@ import { describe, expect, test } from "vitest"; -import { getGraphQLUrl, getJwksUrl } from "./urls"; +import { getJwksUrlFromSaleorApiUrl } from "./urls"; describe("urls.ts", () => { - describe("jwksUrl function", () => { - test.each([ - ["localhost:8000", "http://localhost:8000/.well-known/jwks.json"], - [ - "https://my-saleor.saleor.cloud", - "https://https://my-saleor.saleor.cloud/.well-known/jwks.json", - ], - ])("resolves %s to be %s", (input, expectedOutput) => { - expect(getJwksUrl(input)).toBe(expectedOutput); - }); - }); - - describe("graphQLUrl function", () => { - test.each([ - ["localhost:8000", "http://localhost:8000/graphql/"], - ["https://my-saleor.saleor.cloud", "https://https://my-saleor.saleor.cloud/graphql/"], - ])("resolves %s to be %s", (input, expectedOutput) => { - expect(getGraphQLUrl(input)).toBe(expectedOutput); + describe("getJwksUrlFromSaleorApiUrl function", () => { + test("Resolves valid url from saleor api url", () => { + expect(getJwksUrlFromSaleorApiUrl("https://my-saleor.saleor.cloud")).toBe( + "https://my-saleor.saleor.cloud/.well-known/jwks.json" + ); }); }); }); diff --git a/src/urls.ts b/src/urls.ts index 9b78af9..9006b4d 100644 --- a/src/urls.ts +++ b/src/urls.ts @@ -1,30 +1,2 @@ -/** - * TODO: Validate proper URL - */ -const resolveUrlProtocol = (saleorDomain: string): string => - saleorDomain === "localhost:8000" ? "http" : "https"; - -/** - * @deprecated use getJwksUrlFromSaleorApiUrl - */ -export const getJwksUrl = (saleorDomain: string): string => - `${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 => - `${resolveUrlProtocol(saleorDomain)}://${saleorDomain}/graphql/`; - -/** - * @deprecated Remove in v1, left for compatibility - */ -export const jwksUrl = getJwksUrl; - -/** - * @deprecated Remove in v1, left for compatibility - */ -export const graphQLUrl = getGraphQLUrl; diff --git a/src/verify-signature.ts b/src/verify-signature.ts index 4e3c52e..78f7f41 100644 --- a/src/verify-signature.ts +++ b/src/verify-signature.ts @@ -1,40 +1,17 @@ import * as jose from "jose"; import { createDebug } from "./debug"; -import { getJwksUrl, getJwksUrlFromSaleorApiUrl } from "./urls"; +import { getJwksUrlFromSaleorApiUrl } from "./urls"; const debug = createDebug("verify-signature"); /** - * @deprecated - * use verifySignatureFromApiUrl - */ -export const verifySignature = async (domain: 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(getJwksUrl(domain)) - ) 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"); - } -}; - -/** - * Verify payload signature with public key of given `domain` + * Verify Webhook payload signature with public key of given `domain` * https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks#payload-signature + * + * Use Saleor URL to fetch JWKS + * + * TODO: Add test */ export const verifySignatureFromApiUrl = async ( saleorApiUrl: string, @@ -63,6 +40,12 @@ export const verifySignatureFromApiUrl = async ( } }; +/** + * Verify the Webhook payload signature from provided JWKS string. + * JWKS can be cached to avoid unnecessary calls. + * + * TODO: Add test + */ export const verifySignatureWithJwks = async (jwks: string, signature: string, rawBody: string) => { const [header, , jwsSignature] = signature.split("."); const jws: jose.FlattenedJWSInput = { diff --git a/tsup.config.ts b/tsup.config.ts index 6fb9644..f0104df 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ "src/headers.ts", "src/saleor-app.ts", "src/verify-jwt.ts", + "src/verify-signature.ts", "src/APL/index.ts", "src/app-bridge/index.ts", "src/app-bridge/next/index.ts",