I’m building a Shopify public app and I’m having trouble with the HMAC verification process. Despite using the Shopify Secret from the Dashboard of my app at partners.shopify.com, I can’t get the computed HMAC and the received HMAC to match. This always results in a failed verification.
I’ve tried various combinations, but nothing seems to work—they never match. There might be something I’m missing. I hope someone here has a solution.
Here is the current implementation using FastAPI.
def verify_webhook(data, hmac_header):
# Compute HMAC using the raw data
secret = SHOPIFY_SECRET.encode('utf-8')
digest = hmac.new(secret, data, digestmod=sha256).digest()
computed_hmac = base64.b64encode(digest).decode('utf-8')
print("SECRET:", secret)
print("DATA (raw):", data)
print("DIGEST (binary):", digest)
print("DIGEST (hex):", digest.hex())
print("COMPUTED HMAC (base64):", computed_hmac)
print("RECEIVED HMAC (header):", hmac_header)
return hmac.compare_digest(computed_hmac, hmac_header)
@app.post('/webhook/customer/data_request')
async def customer_data_request_webhook(request: Request):
try:
data = await request.body()
headers = dict(request.headers)
print('RAW DATA:', data)
print('HEADERS:', headers)
hmac_header = headers.get('X-Shopify-Hmac-SHA256')
print("HMAC HEADER:", hmac_header)
verified = verify_webhook(data, hmac_header)
if not verified:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="HMAC verification failed")
print("Received customer data request webhook:")
print(data)
return Response(status_code=HTTP_200_OK)
Thank you in advance, Cheers, Kevin
here are logs by the way
DATA b'{"shop_id":60225617942,"shop_domain":"app-security.myshopify.com"}'
HEADER {'host': '*******************', 'user-agent': 'Ruby', 'content-length': '66', 'accept': '*/*', 'accept-encoding': 'gzip', 'cdn-loop': 'cloudflare; subreqs=1', 'cf-connecting-ip': '34.23.252.48', 'cf-ew-via': '15', 'cf-ipcountry': 'US', 'cf-ray': '88f83610910badb0-ATL', 'cf-visitor': '{"scheme":"https"}', 'cf-worker': 'onrender.com', 'content-type': 'application/json', 'render-proxy-ttl': '4', 'rndr-id': '4e79bfd2-668d-4233', 'traceparent': '00-61fe6df471212a349f78bfef3aeb53bb-b94c865665aa5ace-01', 'true-client-ip': '34.23.252.48', 'x-cloud-trace-context': '61fe6df471212a349f78bfef3aeb53bb/13352194700877650638;o=1', 'x-forwarded-for': '34.23.252.48, 10.216.35.99', 'x-forwarded-proto': 'https', 'x-request-start': '1717674903202354', 'x-shopify-api-version': '2022-10', 'x-shopify-hmac-sha256': '4Apq2emHXH+wFrY3yTP9u+XUAMrb/pOl/HE0OMmqf1E=', 'x-shopify-shop-domain': 'app-security.myshopify.com', 'x-shopify-test': 'true', 'x-shopify-topic': 'shop/redact', 'x-shopify-trace-context': '61fe6df471212a349f78bfef3aeb53bb/13352194700877650638;o=1', 'x-shopify-trace-hint': 'true', 'x-shopify-triggered-at': '2024-06-06T11:55:03.059362171Z', 'x-shopify-webhook-id': '3c7cc95f-e354-46f4-87b4-8da946b988df'}
HMAC HEADER 4Apq2emHXH+wFrY3yTP9u+XUAMrb/pOl/HE0OMmqf1E=
SECRET: b'********************************'
DATA (raw): b'{"shop_id":60225617942,"shop_domain":"app-security.myshopify.com"}'
DIGEST (binary): b':\x00\xe2\x01\xf5\x88\x8c\xfb,H\\\x88\x9a\xe6\xd7\xaa\x13\x18M\xd2\xcdY\x00\x16L\x12\xda\xbf-\x88N\x16'
COMPUTED HMAC (base64): OgDiAfWIjPssSFyImubXqhMYTdLNWQAWTBLavy2IThY=
RECEIVED HMAC (header): 4Apq2emHXH+wFrY3yTP9u+XUAMrb/pOl/HE0OMmqf1E=
Error processing shop data erasure webhook:
401: HMAC verification failed
As per our discussion above, I am sharing the logic in JS that worked for me.
const verifyShopifyRequest = async (shopifySecret, query) => {
try {
if (!(query && shopifySecret)){
console.log(`Invalid query or secret <<--------- verifyShopifyRequest = ERROR`);
return false;
}
const { signature = "", ...otherQueryParams } = query;
const input = Object.keys(otherQueryParams)
.sort()
.map((key) => {
const value = otherQueryParams[key];
return `${key}=${value}`;
})
.join("");
const hmac = crypto.createHmac("sha256", shopifySecret).update(input).digest("hex");
const digest = Buffer.from(hmac, "utf-8");
const checksum = Buffer.from(signature, "utf-8");
if(!(digest.length === checksum.length && crypto.timingSafeEqual(digest, checksum))) return false;
return true;
} catch (error) {
console.log(error, ` <<--------- verifyShopifyRequest = ERROR`);
return false;
}
}
A possible equivalent for the above logic in Python:
import hashlib
import hmac
def verify_shopify_request(shopify_secret, query):
try:
if not (query and shopify_secret):
print("Invalid query or secret <<--------- verify_shopify_request = ERROR")
return False
signature = query.pop('signature', '')
input_string = ''.join(f"{key}={value}" for key, value in sorted(query.items()))
hmac_digest = hmac.new(shopify_secret.encode('utf-8'), input_string.encode('utf-8'), hashlib.sha256).hexdigest()
if not (len(hmac_digest) == len(signature) and hmac.compare_digest(hmac_digest, signature)):
return False
return True
except Exception as error:
print(f"{error} <<--------- verify_shopify_request = ERROR")
return False