From fe2921dcdcf8e5e9cc38658ff348a4883cc6474f Mon Sep 17 00:00:00 2001 From: Krzysztof Wolski Date: Thu, 4 Aug 2022 18:37:31 +0200 Subject: [PATCH] Migrate to Jose and better error messages --- src/middleware.ts | 81 ++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index a32a4ef..d454028 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,11 +1,10 @@ import crypto from "crypto"; import * as jose from "jose"; -import jwt, { JwtPayload } from "jsonwebtoken"; -import jwks, { CertSigningKey, RsaSigningKey } from "jwks-rsa"; import type { Middleware, Request } from "retes"; import { Response } from "retes/response"; -import { SALEOR_DOMAIN_HEADER, SALEOR_EVENT_HEADER } from "./const"; +import { SALEOR_AUTHORIZATION_BEARER_HEADER } from "./const"; +import { getSaleorHeaders } from "./headers"; import { jwksUrl } from "./urls"; export const withBaseURL: Middleware = (handler) => async (request) => { @@ -18,9 +17,9 @@ export const withBaseURL: Middleware = (handler) => async (request) => { }; export const withSaleorDomainPresent: Middleware = (handler) => async (request) => { - const saleorDomain = request.headers[SALEOR_DOMAIN_HEADER]; + const { domain } = getSaleorHeaders(request.headers); - if (!saleorDomain) { + if (!domain) { return Response.BadRequest({ success: false, message: "Missing Saleor domain header.", @@ -34,11 +33,12 @@ export const withSaleorEventMatch = (expectedEvent: `${Lowercase}`): Middleware => (handler) => async (request) => { - const receivedEvent = request.headers[SALEOR_EVENT_HEADER]; - if (receivedEvent !== expectedEvent) { + const { event } = getSaleorHeaders(request.headers); + + if (event !== expectedEvent) { return Response.BadRequest({ success: false, - message: "Invalid Saleor Event", + message: `Invalid Saleor event. Expecting ${expectedEvent}.`, }); } @@ -61,15 +61,16 @@ export const withWebhookSignatureVerified = (secretKey: string | undefined = undefined): Middleware => (handler) => async (request) => { + const ERROR_MESSAGE = "Webhook signature verification failed:"; + if (request.rawBody === undefined) { return Response.InternalServerError({ success: false, - message: "Request payload already parsed.", + message: `${ERROR_MESSAGE} Request payload already parsed.`, }); } - const { [SALEOR_DOMAIN_HEADER]: saleorDomain, "saleor-signature": payloadSignature } = - request.headers; + const { domain: saleorDomain, signature: payloadSignature } = getSaleorHeaders(request.headers); if (secretKey !== undefined) { const calculatedSignature = crypto @@ -80,7 +81,7 @@ export const withWebhookSignatureVerified = if (calculatedSignature !== payloadSignature) { return Response.BadRequest({ success: false, - message: "Invalid signature.", + message: `${ERROR_MESSAGE} Verification using secret key has failed.`, }); } } else { @@ -100,7 +101,7 @@ export const withWebhookSignatureVerified = } catch { return Response.BadRequest({ success: false, - message: "Invalid signature.", + message: `${ERROR_MESSAGE} Verification using public key has failed.`, }); } } @@ -108,7 +109,7 @@ export const withWebhookSignatureVerified = return handler(request); }; -export interface DashboardTokenPayload extends JwtPayload { +export interface DashboardTokenPayload extends jose.JWTPayload { app: string; } @@ -116,37 +117,30 @@ export const withJWTVerified = (getAppId: (request: Request) => Promise): Middleware => (handler) => async (request) => { - const { [SALEOR_DOMAIN_HEADER]: saleorDomain, "authorization-bearer": token } = request.headers; + const { domain, authorizationBearer: token } = getSaleorHeaders(request.headers); + const ERROR_MESSAGE = "JWT verification failed:"; if (token === undefined) { return Response.BadRequest({ success: false, - message: "Missing token.", + message: `${ERROR_MESSAGE} Missing ${SALEOR_AUTHORIZATION_BEARER_HEADER} header.`, }); } - let tokenClaims; + let tokenClaims: DashboardTokenPayload; try { - tokenClaims = jwt.decode(token as string); + tokenClaims = jose.decodeJwt(token as string) as DashboardTokenPayload; } catch (e) { - console.error(e); return Response.BadRequest({ success: false, - message: "Invalid token.", + message: `${ERROR_MESSAGE} Could not decode authorization token.`, }); } - if (tokenClaims === null) { + if (tokenClaims.iss !== domain) { return Response.BadRequest({ success: false, - message: "Invalid token.", - }); - } - - if ((tokenClaims as DashboardTokenPayload).iss !== saleorDomain) { - return Response.BadRequest({ - success: false, - message: "Invalid token.", + message: `${ERROR_MESSAGE} Token iss property is different than domain header.`, }); } @@ -154,35 +148,34 @@ export const withJWTVerified = try { appId = await getAppId(request); } catch (error) { - console.error("Error during getting the app ID."); - console.error(error); - return Response.BadRequest({ + return Response.InternalServerError({ success: false, - message: "Error during token invalidation - could not obtain the app ID.", + message: `${ERROR_MESSAGE} Could not obtain the app ID.`, }); } - if (!appId || (tokenClaims as DashboardTokenPayload).app !== appId) { - return Response.BadRequest({ + if (!appId) { + return Response.InternalServerError({ success: false, - message: "Invalid token.", + message: `${ERROR_MESSAGE} No value for app ID.`, }); } - const jwksClient = jwks({ - jwksUri: `https://${saleorDomain}/.well-known/jwks.json`, - }); - const signingKey = await jwksClient.getSigningKey(); - const signingSecret = - (signingKey as CertSigningKey).publicKey || (signingKey as RsaSigningKey).rsaPublicKey; + if (tokenClaims.app !== appId) { + return Response.BadRequest({ + success: false, + message: `${ERROR_MESSAGE} Token's app property is different than app ID.`, + }); + } try { - jwt.verify(token as string, signingSecret); + const JWKS = jose.createRemoteJWKSet(new URL(jwksUrl(domain))); + await jose.jwtVerify(token, JWKS); } catch (e) { console.error(e); return Response.BadRequest({ success: false, - message: "Invalid token.", + message: `${ERROR_MESSAGE} JWT signature verification failed.`, }); }