@shopify/shopify-app-remix
Version:
Shopify Remix - to simplify the building of Shopify Apps with Remix
234 lines (231 loc) • 9.7 kB
JavaScript
import { CookieNotFound, InvalidHmacError, InvalidOAuthError, HttpResponseError, GraphqlQueryError } from '@shopify/shopify-api';
import { beginAuth } from '../helpers/begin-auth.mjs';
import { redirectToShopifyOrAppRoot } from '../helpers/redirect-to-shopify-or-app-root.mjs';
import '@remix-run/server-runtime';
import { handleClientErrorFactory } from '../helpers/handle-client-error.mjs';
import { redirectToAuthPage } from '../helpers/redirect-to-auth-page.mjs';
import { redirectWithExitIframe } from '../helpers/redirect-with-exitiframe.mjs';
import { getSessionTokenHeader } from '../../helpers/get-session-token-header.mjs';
import { triggerAfterAuthHook } from '../helpers/trigger-after-auth-hook.mjs';
import { validateShopAndHostParams } from '../helpers/validate-shop-and-host-params.mjs';
import 'isbot';
import '../../../types.mjs';
class AuthCodeFlowStrategy {
api;
config;
logger;
constructor({ api, config, logger }) {
this.api = api;
this.config = config;
this.logger = logger;
}
async respondToOAuthRequests(request) {
const { api, config } = this;
const url = new URL(request.url);
const isAuthRequest = url.pathname === config.auth.path;
const isAuthCallbackRequest = url.pathname === config.auth.callbackPath;
if (isAuthRequest || isAuthCallbackRequest) {
const shop = api.utils.sanitizeShop(url.searchParams.get('shop'));
if (!shop)
throw new Response('Shop param is invalid', { status: 400 });
if (isAuthRequest) {
throw await this.handleAuthBeginRequest(request, shop);
}
else {
throw await this.handleAuthCallbackRequest(request, shop);
}
}
if (!getSessionTokenHeader(request)) {
// This is a document request that doesn't contain a session token. We check if the app is installed.
// If the app isn't installed, we initiate the OAuth auth code flow.
// Requests with a header can only happen after the app is installed.
await this.ensureInstalledOnShop(request);
}
}
async authenticate(request, sessionContext) {
const { api, config, logger } = this;
const { shop, session } = sessionContext;
if (!session) {
logger.debug('No session found, redirecting to OAuth', { shop });
await redirectToAuthPage({ config, api }, request, shop);
}
else if (!session.isActive(config.scopes)) {
logger.debug('Found a session, but it has expired, redirecting to OAuth', { shop });
await redirectToAuthPage({ config, api }, request, shop);
}
logger.debug('Found a valid session', { shop });
return session;
}
handleClientError(request) {
const { api, config, logger } = this;
return handleClientErrorFactory({
request,
onError: async ({ session, error }) => {
if (error.response.code === 401) {
throw await redirectToAuthPage({ api, config}, request, session.shop);
}
},
});
}
async ensureInstalledOnShop(request) {
const { api, config, logger } = this;
validateShopAndHostParams({ api, config, logger }, request);
const url = new URL(request.url);
let shop = url.searchParams.get('shop');
// Ensure app is installed
logger.debug('Ensuring app is installed on shop', { shop });
if (!(await this.hasValidOfflineId(request))) {
logger.info("Could not find a shop, can't authenticate request");
throw new Response(undefined, {
status: 400,
statusText: 'Bad Request',
});
}
const offlineSession = await this.getOfflineSession(request);
const isEmbedded = url.searchParams.get('embedded') === '1';
if (!offlineSession) {
logger.info("Shop hasn't installed app yet, redirecting to OAuth", {
shop,
});
if (isEmbedded) {
redirectWithExitIframe({ api, config}, request, shop);
}
else {
throw await beginAuth({ api, config}, request, false, shop);
}
}
shop = shop || offlineSession.shop;
if (config.isEmbeddedApp && !isEmbedded) {
try {
logger.debug('Ensuring offline session is valid before embedding', {
shop,
});
await this.testSession(offlineSession);
logger.debug('Offline session is still valid, embedding app', { shop });
}
catch (error) {
await this.handleInvalidOfflineSession(error, request, shop);
}
}
}
async handleAuthBeginRequest(request, shop) {
const { api, config, logger } = this;
logger.info('Handling OAuth begin request', { shop });
// If we're loading from an iframe, we need to break out of it
if (config.isEmbeddedApp &&
request.headers.get('Sec-Fetch-Dest') === 'iframe') {
logger.debug('Auth request in iframe detected, exiting iframe', { shop });
throw redirectWithExitIframe({ api, config}, request, shop);
}
else {
throw await beginAuth({ api, config}, request, false, shop);
}
}
async handleAuthCallbackRequest(request, shop) {
const { api, config, logger } = this;
logger.info('Handling OAuth callback request', { shop });
try {
const { session, headers: responseHeaders } = await api.auth.callback({
rawRequest: request,
});
await config.sessionStorage.storeSession(session);
if (config.useOnlineTokens && !session.isOnline) {
logger.info('Requesting online access token for offline session', {
shop,
});
await beginAuth({ api, config, logger }, request, true, shop);
}
logger.debug('Request is valid, loaded session from OAuth callback', {
shop: session.shop,
isOnline: session.isOnline,
});
await triggerAfterAuthHook({ api, config, logger }, session, request, this);
throw await redirectToShopifyOrAppRoot(request, { api, config, logger }, responseHeaders);
}
catch (error) {
if (error instanceof Response)
throw error;
throw await this.oauthCallbackError(error, request, shop);
}
}
async getOfflineSession(request) {
const offlineId = await this.getOfflineSessionId(request);
return this.config.sessionStorage.loadSession(offlineId);
}
async hasValidOfflineId(request) {
return Boolean(await this.getOfflineSessionId(request));
}
async getOfflineSessionId(request) {
const { api } = this;
const url = new URL(request.url);
const shop = url.searchParams.get('shop');
return shop
? api.session.getOfflineId(shop)
: api.session.getCurrentId({ isOnline: false, rawRequest: request });
}
async testSession(session) {
const { api } = this;
const client = new api.clients.Graphql({
session,
});
await client.request(`#graphql
query shopifyAppShopName {
shop {
name
}
}
`);
}
async oauthCallbackError(error, request, shop) {
const { logger } = this;
logger.error('Error during OAuth callback', { shop, error: error.message });
if (error instanceof CookieNotFound) {
return this.handleAuthBeginRequest(request, shop);
}
if (error instanceof InvalidHmacError ||
error instanceof InvalidOAuthError) {
return new Response(undefined, {
status: 400,
statusText: 'Invalid OAuth Request',
});
}
return new Response(undefined, {
status: 500,
statusText: 'Internal Server Error',
});
}
async handleInvalidOfflineSession(error, request, shop) {
const { api, logger, config } = this;
if (error instanceof HttpResponseError) {
if (error.response.code === 401) {
logger.info('Shop session is no longer valid, redirecting to OAuth', {
shop,
});
throw await beginAuth({ api, config}, request, false, shop);
}
else {
const message = JSON.stringify(error.response.body, null, 2);
logger.error(`Unexpected error during session validation: ${message}`, {
shop,
});
throw new Response(undefined, {
status: error.response.code,
statusText: error.response.statusText,
});
}
}
else if (error instanceof GraphqlQueryError) {
const context = { shop };
if (error.response) {
context.response = JSON.stringify(error.body);
}
logger.error(`Unexpected error during session validation: ${error.message}`, context);
throw new Response(undefined, {
status: 500,
statusText: 'Internal Server Error',
});
}
}
}
export { AuthCodeFlowStrategy };
//# sourceMappingURL=auth-code-flow.mjs.map