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",
|
||||
"import": "./verify-jwt.mjs",
|
||||
"require": "./verify-jwt.js"
|
||||
},
|
||||
"./verify-signature": {
|
||||
"types": "./verify-signature.d.ts",
|
||||
"import": "./verify-signature.mjs",
|
||||
"require": "./verify-signature.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
|
|
|
@ -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<string | undefined>): 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) {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
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 =>
|
||||
`${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 { 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 = {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue