Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle:middleware/auth-express #1436

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-masks-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/shopify-app-express': minor
---

Fixes OPTIONS object inside authenticated session middleware
2 changes: 1 addition & 1 deletion packages/api-clients/storefront-api-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -29,7 +60,6 @@ export function validateAuthenticatedSession({
config.logger.error(
`Error when loading session from storage: ${error}`,
);

handleSessionError(req, res, error);
return undefined;
}
Expand All @@ -42,25 +72,28 @@ 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;
}
}

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,
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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}`,
Expand All @@ -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<string | undefined>} The shop domain if available.
*/
async function setShopFromSessionOrToken(
api: Shopify,
session: Session | undefined,
Expand Down
Loading