import {JwtPayload, Session} from '@shopify/shopify-api'; import type {BasicParams} from '../../types'; import {AppDistribution} from '../../types'; import type {AppConfigArg} from '../../config-types'; import { getSessionTokenHeader, ensureCORSHeadersFactory, getSessionTokenFromUrlParam, respondToBotRequest, respondToOptionsRequest, validateSessionToken, getShopFromRequest, } from '../helpers'; import { cancelBillingFactory, requestBillingFactory, requireBillingFactory, checkBillingFactory, createUsageRecordFactory, updateUsageCappedAmountFactory, } from './billing'; import type { AdminContext, AuthenticateAdmin, EmbeddedAdminContext, NonEmbeddedAdminContext, } from './types'; import { createAdminApiContext, ensureAppIsEmbeddedIfRequired, ensureSessionTokenSearchParamIfRequired, redirectFactory, renderAppBridge, validateShopAndHostParams, } from './helpers'; import {AuthorizationStrategy} from './strategies/types'; import {scopesApiFactory} from './scope/factory'; export interface SessionTokenContext { shop: string; sessionId?: string; sessionToken?: string; payload?: JwtPayload; } interface AuthStrategyParams extends BasicParams { strategy: AuthorizationStrategy; } export function authStrategyFactory({ strategy, ...params }: AuthStrategyParams): AuthenticateAdmin { const {api, logger, config} = params; async function respondToBouncePageRequest(request: Request) { const url = new URL(request.url); if (url.pathname.endsWith(config.auth.patchSessionTokenPath)) { logger.debug('Rendering bounce page', { shop: getShopFromRequest(request), }); throw renderAppBridge({config, logger, api}, request); } } async function respondToExitIframeRequest(request: Request) { const url = new URL(request.url); if (url.pathname.endsWith(config.auth.exitIframePath)) { const destination = url.searchParams.get('exitIframe')!; logger.debug('Rendering exit iframe page', { shop: getShopFromRequest(request), destination, }); throw renderAppBridge({config, logger, api}, request, {url: destination}); } } type AdminContextBase = | EmbeddedAdminContext | NonEmbeddedAdminContext; function createContext( request: Request, session: Session, authStrategy: AuthorizationStrategy, sessionToken?: JwtPayload, ): AdminContext { let context: AdminContextBase = { admin: createAdminApiContext( session, params, authStrategy.handleClientError(request), ), billing: { require: requireBillingFactory(params, request, session), check: checkBillingFactory(params, request, session), request: requestBillingFactory(params, request, session), cancel: cancelBillingFactory(params, request, session), createUsageRecord: createUsageRecordFactory(params, request, session), updateUsageCappedAmount: updateUsageCappedAmountFactory( params, request, session, ), }, session, cors: ensureCORSHeadersFactory(params, request), }; context = addEmbeddedFeatures(context, request, session, sessionToken); context = addScopesFeatures(context); return context as AdminContext; } function addEmbeddedFeatures( context: AdminContextBase, request: Request, session: Session, sessionToken?: JwtPayload, ) { if (config.distribution === AppDistribution.ShopifyAdmin) { return context; } return { ...context, sessionToken, redirect: redirectFactory(params, request, session.shop), }; } function addScopesFeatures(context: AdminContextBase) { return { ...context, scopes: scopesApiFactory(params, context.session, context.admin), }; } return async function authenticateAdmin(request: Request) { try { respondToBotRequest(params, request); respondToOptionsRequest(params, request); await respondToBouncePageRequest(request); await respondToExitIframeRequest(request); // If this is a valid request, but it doesn't have a session token header, this is a document request. We need to // ensure we're embedded if needed and we have the information needed to load the session. if (!getSessionTokenHeader(request)) { validateShopAndHostParams(params, request); await ensureAppIsEmbeddedIfRequired(params, request); await ensureSessionTokenSearchParamIfRequired(params, request); } logger.info('Authenticating admin request', { shop: getShopFromRequest(request), }); const {payload, shop, sessionId, sessionToken} = await getSessionTokenContext(params, request); logger.debug('Loading session from storage', {shop, sessionId}); const existingSession = sessionId ? await config.sessionStorage!.loadSession(sessionId) : undefined; const session = await strategy.authenticate(request, { session: existingSession, sessionToken, shop, }); return createContext(request, session, strategy, payload); } catch (errorOrResponse) { if (errorOrResponse instanceof Response) { logger.debug('Authenticate returned a response', { shop: getShopFromRequest(request), }); ensureCORSHeadersFactory(params, request)(errorOrResponse); } throw errorOrResponse; } }; } async function getSessionTokenContext( params: BasicParams, request: Request, ): Promise { const {api, config, logger} = params; const headerSessionToken = getSessionTokenHeader(request); const searchParamSessionToken = getSessionTokenFromUrlParam(request); const sessionToken = (headerSessionToken || searchParamSessionToken)!; logger.debug('Attempting to authenticate session token', { shop: getShopFromRequest(request), sessionToken: JSON.stringify({ header: headerSessionToken, search: searchParamSessionToken, }), }); if (config.distribution !== AppDistribution.ShopifyAdmin) { const payload = await validateSessionToken(params, request, sessionToken); const dest = new URL(payload.dest); const shop = dest.hostname; logger.debug('Session token is valid - authenticated', {shop, payload}); const sessionId = config.useOnlineTokens ? api.session.getJwtSessionId(shop, payload.sub) : api.session.getOfflineId(shop); return {shop, payload, sessionId, sessionToken}; } const url = new URL(request.url); const shop = url.searchParams.get('shop')!; const sessionId = await api.session.getCurrentId({ isOnline: config.useOnlineTokens, rawRequest: request, }); return {shop, sessionId, payload: undefined, sessionToken}; }