Coverage for atlas_ecommerce/security/shopify_hmac.py: 100%
14 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 16:36 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-25 16:36 +0000
1"""Shopify webhook signature verification.
3Shopify signs the raw request body with the app's client secret and sends the
4base64 digest in ``X-Shopify-Hmac-SHA256``. The body must be verified exactly as
5received: re-serialising parsed JSON changes whitespace and key order and will
6never match.
7"""
9import base64
10import hashlib
11import hmac
13HMAC_HEADER = "X-Shopify-Hmac-SHA256"
14WEBHOOK_ID_HEADER = "X-Shopify-Webhook-Id"
15TOPIC_HEADER = "X-Shopify-Topic"
16SHOP_DOMAIN_HEADER = "X-Shopify-Shop-Domain"
19def compute_hmac(raw_body: bytes, secret: str) -> str:
20 """Return the base64 HMAC-SHA256 Shopify would send for this body."""
21 digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
22 return base64.b64encode(digest).decode()
25def verify_shopify_hmac(raw_body: bytes, header_value: str | None, secret: str) -> bool:
26 """Constant-time check of a Shopify webhook signature.
28 Compares the base64 text rather than the decoded bytes: a malformed header
29 would make ``b64decode`` raise, and an exception here is a rejection, not an
30 error worth distinguishing.
31 """
32 if not header_value:
33 return False
34 return hmac.compare_digest(compute_hmac(raw_body, secret), header_value)