diff --git a/.changeset/quiet-masks-fetch.md b/.changeset/quiet-masks-fetch.md new file mode 100644 index 000000000..94d449412 --- /dev/null +++ b/.changeset/quiet-masks-fetch.md @@ -0,0 +1,5 @@ +--- +'@shopify/shopify-app-express': minor +--- + +Fixes OPTIONS object inside authenticated session middleware diff --git a/packages/api-clients/storefront-api-client/README.md b/packages/api-clients/storefront-api-client/README.md index 92730e2a5..eb4034f98 100644 --- a/packages/api-clients/storefront-api-client/README.md +++ b/packages/api-clients/storefront-api-client/README.md @@ -20,7 +20,7 @@ pnpm add @shopify/storefront-api-client ### CDN -The UMD builds of each release version are available via the [`unpkg` CDN](https://unpkg.com/browse/@shopify/storefront-api-client@latest/dist/umd/) +The UMD builds of each release version are available via the [`unpkg` CDN](https://unpkg.com/browse/@shopify/storefront-api-client@latest/dist/umd/) ```html // The minified `0.2.3` version of the Storefront API Client diff --git a/packages/apps/shopify-app-express/src/middlewares/validate-authenticated-session.ts b/packages/apps/shopify-app-express/src/middlewares/validate-authenticated-session.ts index 6d4746c32..952d9476c 100644 --- a/packages/apps/shopify-app-express/src/middlewares/validate-authenticated-session.ts +++ b/packages/apps/shopify-app-express/src/middlewares/validate-authenticated-session.ts @@ -10,6 +10,21 @@ import {hasValidAccessToken} from './has-valid-access-token'; interface validateAuthenticatedSessionParams extends ApiAndConfigParams {} +/** + * Middleware to validate the session for authenticated requests. + * + * This middleware ensures that the incoming request has a valid session, + * and it checks if the session has an active and valid access token. + * + * If the session is invalid, it redirects the user to the authentication flow. + * + * Additionally, this middleware handles preflight `OPTIONS` requests for CORS + * by bypassing the authentication checks and responding with the appropriate + * CORS headers. + * + * @param {validateAuthenticatedSessionParams} params - The parameters required for the middleware, including the API and config. + * @returns {ValidateAuthenticatedSessionMiddleware} The middleware function that validates the session. + */ export function validateAuthenticatedSession({ api, config, @@ -18,6 +33,22 @@ export function validateAuthenticatedSession({ return async (req: Request, res: Response, next: NextFunction) => { config.logger.info('Running validateAuthenticatedSession'); + // Handle preflight OPTIONS requests for CORS + // Bypasses authentication and responds with the necessary CORS headers. + if (req.method === 'OPTIONS') { + res.header('Access-Control-Allow-Origin', '*'); + res.header( + 'Access-Control-Allow-Methods', + 'GET,POST,PUT,DELETE,OPTIONS', + ); + res.header( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization', + ); + // Respond with 200 OK for OPTIONS requests + return res.sendStatus(200); + } + let sessionId: string | undefined; try { sessionId = await api.session.getCurrentId({ @@ -29,7 +60,6 @@ export function validateAuthenticatedSession({ config.logger.error( `Error when loading session from storage: ${error}`, ); - handleSessionError(req, res, error); return undefined; } @@ -42,9 +72,7 @@ export function validateAuthenticatedSession({ config.logger.error( `Error when loading session from storage: ${error}`, ); - - res.status(500); - res.send(error.message); + res.status(500).send(error.message); return undefined; } } @@ -52,15 +80,20 @@ export function validateAuthenticatedSession({ let shop = api.utils.sanitizeShop(req.query.shop as string) || session?.shop; + // Check if the session is associated with the same shop as the request if (session && shop && session.shop !== shop) { config.logger.debug( 'Found a session for a different shop in the request', - {currentShop: session.shop, requestShop: shop}, + { + currentShop: session.shop, + requestShop: shop, + }, ); return redirectToAuth({req, res, api, config}); } + // Validate if the session is active and has a valid access token if (session) { config.logger.debug('Request session found and loaded', { shop: session.shop, @@ -76,6 +109,7 @@ export function validateAuthenticatedSession({ shop: session.shop, }); + // Attach the session to the response's locals for further use res.locals.shopify = { ...res.locals.shopify, session, @@ -85,6 +119,7 @@ export function validateAuthenticatedSession({ } } + // Handle Bearer token in Authorization header for API requests const bearerPresent = req.headers.authorization?.match(/Bearer (.*)/); if (bearerPresent) { if (!shop) { @@ -96,6 +131,7 @@ export function validateAuthenticatedSession({ } } + // Redirect to the authentication flow if the session is not valid const redirectUri = `${config.auth.path}?shop=${shop}`; config.logger.info( `Session was not valid. Redirecting to ${redirectUri}`, @@ -112,19 +148,32 @@ export function validateAuthenticatedSession({ }; } +/** + * Handles session errors by sending the appropriate response to the client. + * + * @param {Request} _req - The Express request object. + * @param {Response} res - The Express response object. + * @param {Error} error - The error encountered while handling the session. + */ function handleSessionError(_req: Request, res: Response, error: Error) { switch (true) { case error instanceof InvalidJwtError: - res.status(401); - res.send(error.message); + res.status(401).send(error.message); break; default: - res.status(500); - res.send(error.message); + res.status(500).send(error.message); break; } } +/** + * Sets the shop value from the session or token for API requests. + * + * @param {Shopify} api - The Shopify API instance. + * @param {Session | undefined} session - The session object. + * @param {string} token - The Bearer token from the Authorization header. + * @returns {Promise} The shop domain if available. + */ async function setShopFromSessionOrToken( api: Shopify, session: Session | undefined,