@shopify/shopify-api
Version:
Shopify API Library for Node - accelerate development with support for authentication, graphql proxy, webhooks
154 lines (151 loc) • 6.66 kB
JavaScript
import { isbot } from 'isbot';
import { throwFailedRequest } from '../../clients/common.mjs';
import ProcessedQuery from '../../utils/processed-query.mjs';
import { BotActivityDetected, CookieNotFound, InvalidOAuthError, PrivateAppError } from '../../error.mjs';
import { validateHmac } from '../../utils/hmac-validator.mjs';
import { sanitizeShop } from '../../utils/shop-validator.mjs';
import { abstractConvertRequest, abstractConvertHeaders, abstractConvertIncomingResponse, abstractConvertResponse } from '../../../runtime/http/index.mjs';
import { logger } from '../../logger/index.mjs';
import { DataType } from '../../clients/types.mjs';
import { fetchRequestFactory } from '../../utils/fetch-request.mjs';
import { STATE_COOKIE_NAME, SESSION_COOKIE_NAME } from './types.mjs';
import { nonce } from './nonce.mjs';
import { safeCompare } from './safe-compare.mjs';
import { createSession } from './create-session.mjs';
import { Cookies } from '../../../runtime/http/cookies.mjs';
const logForBot = ({ request, log, func }) => {
log.debug(`Possible bot request to auth ${func}: `, {
userAgent: request.headers['User-Agent'],
});
};
function begin(config) {
return async ({ shop, callbackPath, isOnline, ...adapterArgs }) => {
throwIfCustomStoreApp(config.isCustomStoreApp, 'Cannot perform OAuth for private apps');
const log = logger(config);
log.info('Beginning OAuth', { shop, isOnline, callbackPath });
const request = await abstractConvertRequest(adapterArgs);
const response = await abstractConvertIncomingResponse(adapterArgs);
let userAgent = request.headers['User-Agent'];
if (Array.isArray(userAgent)) {
userAgent = userAgent[0];
}
if (isbot(userAgent)) {
logForBot({ request, log, func: 'begin' });
response.statusCode = 410;
return abstractConvertResponse(response, adapterArgs);
}
const cookies = new Cookies(request, response, {
keys: [config.apiSecretKey],
secure: true,
});
const state = nonce();
await cookies.setAndSign(STATE_COOKIE_NAME, state, {
expires: new Date(Date.now() + 60000),
sameSite: 'lax',
secure: true,
path: callbackPath,
});
const scopes = config.scopes ? config.scopes.toString() : '';
const query = {
client_id: config.apiKey,
scope: scopes,
redirect_uri: `${config.hostScheme}://${config.hostName}${callbackPath}`,
state,
'grant_options[]': isOnline ? 'per-user' : '',
};
const processedQuery = new ProcessedQuery();
processedQuery.putAll(query);
const cleanShop = sanitizeShop(config)(shop, true);
const redirectUrl = `https://${cleanShop}/admin/oauth/authorize${processedQuery.stringify()}`;
response.statusCode = 302;
response.statusText = 'Found';
response.headers = {
...response.headers,
...cookies.response.headers,
Location: redirectUrl,
};
log.debug(`OAuth started, redirecting to ${redirectUrl}`, { shop, isOnline });
return abstractConvertResponse(response, adapterArgs);
};
}
function callback(config) {
return async function callback({ ...adapterArgs }) {
throwIfCustomStoreApp(config.isCustomStoreApp, 'Cannot perform OAuth for private apps');
const log = logger(config);
const request = await abstractConvertRequest(adapterArgs);
const query = new URL(request.url, `${config.hostScheme}://${config.hostName}`).searchParams;
const shop = query.get('shop');
const response = {};
let userAgent = request.headers['User-Agent'];
if (Array.isArray(userAgent)) {
userAgent = userAgent[0];
}
if (isbot(userAgent)) {
logForBot({ request, log, func: 'callback' });
throw new BotActivityDetected('Invalid OAuth callback initiated by bot');
}
log.info('Completing OAuth', { shop });
const cookies = new Cookies(request, response, {
keys: [config.apiSecretKey],
secure: true,
});
const stateFromCookie = await cookies.getAndVerify(STATE_COOKIE_NAME);
cookies.deleteCookie(STATE_COOKIE_NAME);
if (!stateFromCookie) {
log.error('Could not find OAuth cookie', { shop });
throw new CookieNotFound(`Cannot complete OAuth process. Could not find an OAuth cookie for shop url: ${shop}`);
}
const authQuery = Object.fromEntries(query.entries());
if (!(await validQuery({ config, query: authQuery, stateFromCookie }))) {
log.error('Invalid OAuth callback', { shop, stateFromCookie });
throw new InvalidOAuthError('Invalid OAuth callback.');
}
log.debug('OAuth request is valid, requesting access token', { shop });
const body = {
client_id: config.apiKey,
client_secret: config.apiSecretKey,
code: query.get('code'),
};
const cleanShop = sanitizeShop(config)(query.get('shop'), true);
const postResponse = await fetchRequestFactory(config)(`https://${cleanShop}/admin/oauth/access_token`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': DataType.JSON,
Accept: DataType.JSON,
},
});
if (!postResponse.ok) {
throwFailedRequest(await postResponse.json(), false, postResponse);
}
const session = createSession({
accessTokenResponse: await postResponse.json(),
shop: cleanShop,
state: stateFromCookie,
config,
});
if (!config.isEmbeddedApp) {
await cookies.setAndSign(SESSION_COOKIE_NAME, session.id, {
expires: session.expires,
sameSite: 'lax',
secure: true,
path: '/',
});
}
return {
headers: (await abstractConvertHeaders(cookies.response.headers, adapterArgs)),
session,
};
};
}
async function validQuery({ config, query, stateFromCookie, }) {
return ((await validateHmac(config)(query)) &&
safeCompare(query.state, stateFromCookie));
}
function throwIfCustomStoreApp(isCustomStoreApp, message) {
if (isCustomStoreApp) {
throw new PrivateAppError(message);
}
}
export { begin, callback };
//# sourceMappingURL=oauth.mjs.map