123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
import { verifyJWT } from "@saleor/app-sdk/verify-jwt";
|
|
import { logger, redactLogValue } from "../../lib/logger";
|
|
import { REQUIRED_SALEOR_PERMISSIONS } from "../jwt/consts";
|
|
import { middleware, procedure } from "./trpc-server";
|
|
import { checkTokenExpiration } from "@/modules/jwt/check-token-expiration";
|
|
import { saleorApp } from "@/saleor-app";
|
|
import { createClient } from "@/lib/create-graphq-client";
|
|
import {
|
|
JwtInvalidError,
|
|
JwtTokenExpiredError,
|
|
ReqMissingAppIdError,
|
|
ReqMissingAuthDataError,
|
|
ReqMissingSaleorApiUrlError,
|
|
ReqMissingTokenError,
|
|
} from "@/errors";
|
|
|
|
const attachAppToken = middleware(async ({ ctx, next }) => {
|
|
logger.debug("attachAppToken middleware");
|
|
|
|
if (!ctx.saleorApiUrl) {
|
|
logger.debug("ctx.saleorApiUrl not found, throwing");
|
|
|
|
throw new ReqMissingSaleorApiUrlError("Missing saleorApiUrl in request");
|
|
}
|
|
|
|
const authData = await saleorApp.apl.get(ctx.saleorApiUrl);
|
|
|
|
if (!authData) {
|
|
logger.debug("authData not found, throwing 401");
|
|
|
|
throw new ReqMissingAuthDataError("Missing authData in request");
|
|
}
|
|
|
|
return next({
|
|
ctx: {
|
|
appToken: authData.token,
|
|
saleorApiUrl: authData.saleorApiUrl,
|
|
appId: authData.appId,
|
|
},
|
|
});
|
|
});
|
|
|
|
const validateClientToken = middleware(async ({ ctx, next }) => {
|
|
logger.debug("validateClientToken middleware");
|
|
|
|
if (!ctx.token) {
|
|
throw new ReqMissingTokenError(
|
|
"Missing token in request. This middleware can be used only in frontend",
|
|
);
|
|
}
|
|
|
|
if (!ctx.appId) {
|
|
throw new ReqMissingAppIdError(
|
|
"Missing appId in request. This middleware can be used after auth is attached",
|
|
);
|
|
}
|
|
|
|
if (!ctx.saleorApiUrl) {
|
|
throw new ReqMissingSaleorApiUrlError(
|
|
"Missing saleorApiUrl in request. This middleware can be used after auth is attached",
|
|
);
|
|
}
|
|
|
|
logger.debug({ token: redactLogValue(ctx.token) }, "check if JWT token didn't expire");
|
|
|
|
const expired = checkTokenExpiration(ctx.token);
|
|
logger.debug({ expired }, "JWT token expiration check result");
|
|
if (expired) {
|
|
throw new JwtTokenExpiredError("Token expired");
|
|
}
|
|
|
|
try {
|
|
logger.debug({ token: redactLogValue(ctx.token) }, "trying to verify JWT token from frontend");
|
|
|
|
await verifyJWT({
|
|
appId: ctx.appId,
|
|
token: ctx.token,
|
|
saleorApiUrl: ctx.saleorApiUrl,
|
|
requiredPermissions: REQUIRED_SALEOR_PERMISSIONS,
|
|
});
|
|
} catch (e) {
|
|
logger.debug("JWT verification failed, throwing");
|
|
throw new JwtInvalidError("Invalid token", { cause: e });
|
|
}
|
|
|
|
return next({
|
|
ctx: {
|
|
...ctx,
|
|
saleorApiUrl: ctx.saleorApiUrl,
|
|
},
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Construct common graphQL client and attach it to the context
|
|
*
|
|
* Can be used only if called from the frontend (react-query),
|
|
* otherwise jwks validation will fail (if createCaller used)
|
|
*
|
|
* TODO Rethink middleware composition to enable safe server-side router calls
|
|
*/
|
|
export const protectedClientProcedure = procedure
|
|
.use(attachAppToken)
|
|
.use(validateClientToken)
|
|
.use(async ({ ctx, next }) => {
|
|
const client = createClient(ctx.saleorApiUrl, async () =>
|
|
Promise.resolve({ token: ctx.appToken }),
|
|
);
|
|
|
|
const pinoLoggerInstance = logger.child({
|
|
saleorApiUrl: ctx.saleorApiUrl,
|
|
});
|
|
|
|
return next({
|
|
ctx: {
|
|
apiClient: client,
|
|
appToken: ctx.appToken,
|
|
saleorApiUrl: ctx.saleorApiUrl,
|
|
logger: pinoLoggerInstance,
|
|
},
|
|
});
|
|
});
|