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