UNPKG

@shopify/shopify-api

Version:

Shopify API Library for Node - accelerate development with support for authentication, graphql proxy, webhooks

98 lines (95 loc) 3.99 kB
import { logger } from '../logger/index.mjs'; import { ShopifyHeader } from '../types.mjs'; import { abstractConvertRequest } from '../../runtime/http/index.mjs'; import { HashFormat } from '../../runtime/crypto/types.mjs'; import { createSHA256HMAC } from '../../runtime/crypto/utils.mjs'; import { InvalidHmacError } from '../error.mjs'; import { safeCompare } from '../auth/oauth/safe-compare.mjs'; import ProcessedQuery from './processed-query.mjs'; import { ValidationErrorReason } from './types.mjs'; import { getHeader } from '../../runtime/http/headers.mjs'; const HMAC_TIMESTAMP_PERMITTED_CLOCK_TOLERANCE_SEC = 90; function stringifyQueryForAdmin(query) { const processedQuery = new ProcessedQuery(); Object.keys(query) .sort((val1, val2) => val1.localeCompare(val2)) .forEach((key) => processedQuery.put(key, query[key])); return processedQuery.stringify(true); } function stringifyQueryForAppProxy(query) { return Object.entries(query) .sort(([val1], [val2]) => val1.localeCompare(val2)) .reduce((acc, [key, value]) => { return `${acc}${key}=${Array.isArray(value) ? value.join(',') : value}`; }, ''); } function generateLocalHmac(config) { return async (params, signator = 'admin') => { const { hmac, signature, ...query } = params; const queryString = signator === 'admin' ? stringifyQueryForAdmin(query) : stringifyQueryForAppProxy(query); return createSHA256HMAC(config.apiSecretKey, queryString, HashFormat.Hex); }; } function validateHmac(config) { return async (query, { signator } = { signator: 'admin' }) => { if (signator === 'admin' && !query.hmac) { throw new InvalidHmacError('Query does not contain an HMAC value.'); } if (signator === 'appProxy' && !query.signature) { throw new InvalidHmacError('Query does not contain a signature value.'); } validateHmacTimestamp(query); const hmac = signator === 'appProxy' ? query.signature : query.hmac; const localHmac = await generateLocalHmac(config)(query, signator); return safeCompare(hmac, localHmac); }; } async function validateHmacString(config, data, hmac, format) { const localHmac = await createSHA256HMAC(config.apiSecretKey, data, format); return safeCompare(hmac, localHmac); } function getCurrentTimeInSec() { return Math.trunc(Date.now() / 1000); } function validateHmacFromRequestFactory(config) { return async function validateHmacFromRequest({ type, rawBody, ...adapterArgs }) { const request = await abstractConvertRequest(adapterArgs); if (!rawBody.length) { return fail(ValidationErrorReason.MissingBody, type, config); } const hmac = getHeader(request.headers, ShopifyHeader.Hmac); if (!hmac) { return fail(ValidationErrorReason.MissingHmac, type, config); } const validHmac = await validateHmacString(config, rawBody, hmac, HashFormat.Base64); if (!validHmac) { return fail(ValidationErrorReason.InvalidHmac, type, config); } return succeed(type, config); }; } function validateHmacTimestamp(query) { if (Math.abs(getCurrentTimeInSec() - Number(query.timestamp)) > HMAC_TIMESTAMP_PERMITTED_CLOCK_TOLERANCE_SEC) { throw new InvalidHmacError('HMAC timestamp is outside of the tolerance range'); } } async function fail(reason, type, config) { const log = logger(config); await log.debug(`${type} request is not valid`, { reason }); return { valid: false, reason, }; } async function succeed(type, config) { const log = logger(config); await log.debug(`${type} request is valid`); return { valid: true, }; } export { generateLocalHmac, getCurrentTimeInSec, validateHmac, validateHmacFromRequestFactory, validateHmacString }; //# sourceMappingURL=hmac-validator.mjs.map