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

1"""Shopify webhook signature verification. 

2 

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

8 

9import base64 

10import hashlib 

11import hmac 

12 

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" 

17 

18 

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() 

23 

24 

25def verify_shopify_hmac(raw_body: bytes, header_value: str | None, secret: str) -> bool: 

26 """Constant-time check of a Shopify webhook signature. 

27 

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)