Expose verify-jwks and remove legacy domain usage instead of api url (#173)
* Expose verify-jwks and remove legacy domain usage instead of api url * Update src/verify-signature.ts Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com> --------- Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
This commit is contained in:
parent
a15d72602f
commit
652833146f
7 changed files with 40 additions and 89 deletions
|
@ -132,6 +132,11 @@
|
||||||
"types": "./verify-jwt.d.ts",
|
"types": "./verify-jwt.d.ts",
|
||||||
"import": "./verify-jwt.mjs",
|
"import": "./verify-jwt.mjs",
|
||||||
"require": "./verify-jwt.js"
|
"require": "./verify-jwt.js"
|
||||||
|
},
|
||||||
|
"./verify-signature": {
|
||||||
|
"types": "./verify-signature.d.ts",
|
||||||
|
"import": "./verify-signature.mjs",
|
||||||
|
"require": "./verify-signature.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|
|
@ -2,9 +2,9 @@ import * as jose from "jose";
|
||||||
import type { Middleware, Request } from "retes";
|
import type { Middleware, Request } from "retes";
|
||||||
import { Response } from "retes/response";
|
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 { getSaleorHeaders } from "../headers";
|
||||||
import { getJwksUrl } from "../urls";
|
import { getJwksUrlFromSaleorApiUrl } from "../urls";
|
||||||
import { createMiddlewareDebug } from "./middleware-debug";
|
import { createMiddlewareDebug } from "./middleware-debug";
|
||||||
|
|
||||||
const debug = createMiddlewareDebug("withJWTVerified");
|
const debug = createMiddlewareDebug("withJWTVerified");
|
||||||
|
@ -19,9 +19,9 @@ export const withJWTVerified =
|
||||||
(getAppId: (request: Request) => Promise<string | undefined>): Middleware =>
|
(getAppId: (request: Request) => Promise<string | undefined>): Middleware =>
|
||||||
(handler) =>
|
(handler) =>
|
||||||
async (request) => {
|
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") {
|
if (typeof token !== "string") {
|
||||||
debug("Middleware with empty token, will response with Bad Request", token);
|
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));
|
debug("Middleware called with token starting with: \"%s\"", token.substring(0, 4));
|
||||||
|
|
||||||
if (domain === undefined) {
|
if (saleorApiUrl === undefined) {
|
||||||
return Response.BadRequest({
|
return Response.BadRequest({
|
||||||
success: false,
|
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 {
|
try {
|
||||||
debug("Trying to create JWKS");
|
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");
|
debug("Trying to compare JWKS with token");
|
||||||
await jose.jwtVerify(token, JWKS);
|
await jose.jwtVerify(token, JWKS);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -2,15 +2,18 @@ import crypto from "crypto";
|
||||||
import { Middleware } from "retes";
|
import { Middleware } from "retes";
|
||||||
import { Response } from "retes/response";
|
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 { getSaleorHeaders } from "../headers";
|
||||||
import { verifySignature } from "../verify-signature";
|
import { verifySignatureFromApiUrl } from "../verify-signature";
|
||||||
import { createMiddlewareDebug } from "./middleware-debug";
|
import { createMiddlewareDebug } from "./middleware-debug";
|
||||||
|
|
||||||
const debug = createMiddlewareDebug("withWebhookSignatureVerified");
|
const debug = createMiddlewareDebug("withWebhookSignatureVerified");
|
||||||
|
|
||||||
const ERROR_MESSAGE = "Webhook signature verification failed:";
|
const ERROR_MESSAGE = "Webhook signature verification failed:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Add test
|
||||||
|
*/
|
||||||
export const withWebhookSignatureVerified =
|
export const withWebhookSignatureVerified =
|
||||||
(secretKey: string | undefined = undefined): Middleware =>
|
(secretKey: string | undefined = undefined): Middleware =>
|
||||||
(handler) =>
|
(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) {
|
if (!payloadSignature) {
|
||||||
debug("Signature header was not found");
|
debug("Signature header was not found");
|
||||||
|
@ -37,10 +40,10 @@ export const withWebhookSignatureVerified =
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!saleorDomain) {
|
if (!saleorApiUrl) {
|
||||||
return Response.BadRequest({
|
return Response.BadRequest({
|
||||||
success: false,
|
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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
await verifySignature(saleorDomain, payloadSignature, request.rawBody);
|
await verifySignatureFromApiUrl(saleorApiUrl, payloadSignature, request.rawBody);
|
||||||
debug("JWKS verified");
|
debug("JWKS verified");
|
||||||
} catch {
|
} catch {
|
||||||
debug("JWKS verification failed");
|
debug("JWKS verification failed");
|
||||||
|
|
|
@ -1,26 +1,13 @@
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { getGraphQLUrl, getJwksUrl } from "./urls";
|
import { getJwksUrlFromSaleorApiUrl } from "./urls";
|
||||||
|
|
||||||
describe("urls.ts", () => {
|
describe("urls.ts", () => {
|
||||||
describe("jwksUrl function", () => {
|
describe("getJwksUrlFromSaleorApiUrl function", () => {
|
||||||
test.each([
|
test("Resolves valid url from saleor api url", () => {
|
||||||
["localhost:8000", "http://localhost:8000/.well-known/jwks.json"],
|
expect(getJwksUrlFromSaleorApiUrl("https://my-saleor.saleor.cloud")).toBe(
|
||||||
[
|
"https://my-saleor.saleor.cloud/.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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
28
src/urls.ts
28
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 =>
|
export const getJwksUrlFromSaleorApiUrl = (saleorApiUrl: string): string =>
|
||||||
`${new URL(saleorApiUrl).origin}/.well-known/jwks.json`;
|
`${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;
|
|
||||||
|
|
|
@ -1,40 +1,17 @@
|
||||||
import * as jose from "jose";
|
import * as jose from "jose";
|
||||||
|
|
||||||
import { createDebug } from "./debug";
|
import { createDebug } from "./debug";
|
||||||
import { getJwksUrl, getJwksUrlFromSaleorApiUrl } from "./urls";
|
import { getJwksUrlFromSaleorApiUrl } from "./urls";
|
||||||
|
|
||||||
const debug = createDebug("verify-signature");
|
const debug = createDebug("verify-signature");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* Verify Webhook payload signature with public key of given `domain`
|
||||||
* 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`
|
|
||||||
* https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks#payload-signature
|
* 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 (
|
export const verifySignatureFromApiUrl = async (
|
||||||
saleorApiUrl: string,
|
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) => {
|
export const verifySignatureWithJwks = async (jwks: string, signature: string, rawBody: string) => {
|
||||||
const [header, , jwsSignature] = signature.split(".");
|
const [header, , jwsSignature] = signature.split(".");
|
||||||
const jws: jose.FlattenedJWSInput = {
|
const jws: jose.FlattenedJWSInput = {
|
||||||
|
|
|
@ -8,6 +8,7 @@ export default defineConfig({
|
||||||
"src/headers.ts",
|
"src/headers.ts",
|
||||||
"src/saleor-app.ts",
|
"src/saleor-app.ts",
|
||||||
"src/verify-jwt.ts",
|
"src/verify-jwt.ts",
|
||||||
|
"src/verify-signature.ts",
|
||||||
"src/APL/index.ts",
|
"src/APL/index.ts",
|
||||||
"src/app-bridge/index.ts",
|
"src/app-bridge/index.ts",
|
||||||
"src/app-bridge/next/index.ts",
|
"src/app-bridge/next/index.ts",
|
||||||
|
|
Loading…
Reference in a new issue