UNPKG

@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
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