Shopify HMAC parameter verification failing in Python
Question:
I’m having some trouble verifying the HMAC parameter coming from Shopify. The code I’m using per the Shopify documentation is returning an incorrect result.
Here’s my annotated code:
import urllib
import hmac
import hashlib
qs = "hmac=96d0a58213b6aa5ca5ef6295023a90694cf21655cf301975978a9aa30e2d3e48&locale=en&protocol=https%3A%2F%2F&shop=myshopname.myshopify.com×tamp=1520883022"
Parse the querystring
params = urllib.parse.parse_qs(qs)
Extract the hmac value
value = params['hmac'][0]
Remove parameters from the querystring per documentation
del params['hmac']
del params['signature']
Recombine the parameters
new_qs = urllib.parse.urlencode(params)
Calculate the digest
h = hmac.new(SECRET.encode("utf8"), msg=new_qs.encode("utf8"), digestmod=hashlib.sha256)
Returns False
!
hmac.compare_digest(h.hexdigest(), value)
That last step should, ostensibly, return true. Every step followed here is outlined as commented in the Shopify docs.
Answers:
At some point, recently, Shopify started including the protocol
parameter in the querystring payload. This itself wouldn’t be a problem, except for the fact that Shopify doesn’t document that :
and /
are not to be URL-encoded when checking the signature. This is unexpected, given that they themselves do URL-encode these characters in the query string that is provided.
To fix the issue, provide the safe
parameter to urllib.parse.urlencode
with the value :/
(fitting, right?). The full working code looks like this:
params = urllib.parse.parse_qsl(qs)
cleaned_params = []
hmac_value = dict(params)['hmac']
# Sort parameters
for (k, v) in sorted(params):
if k in ['hmac', 'signature']:
continue
cleaned_params.append((k, v))
new_qs = urllib.parse.urlencode(cleaned_params, safe=":/")
secret = SECRET.encode("utf8")
h = hmac.new(secret, msg=new_qs.encode("utf8"), digestmod=hashlib.sha256)
# Compare digests
hmac.compare_digest(h.hexdigest(), hmac_value)
Hope this is helpful for others running into this issue!
import hmac
import hashlib
...
# Inside your view in Django's views.py
params = request.GET.dict()
#
myhmac = params.pop('hmac')
params['state'] = int(params['state'])
line = '&'.join([
'%s=%s' % (key, value)
for key, value in sorted(params.items())
])
print(line)
h = hmac.new(
key=SHARED_SECRET.encode('utf-8'),
msg=line.encode('utf-8'),
digestmod=hashlib.sha256
)
# Cinderella ?
print(hmac.compare_digest(h.hexdigest(), myhmac))
I’m having some trouble verifying the HMAC parameter coming from Shopify. The code I’m using per the Shopify documentation is returning an incorrect result.
Here’s my annotated code:
import urllib
import hmac
import hashlib
qs = "hmac=96d0a58213b6aa5ca5ef6295023a90694cf21655cf301975978a9aa30e2d3e48&locale=en&protocol=https%3A%2F%2F&shop=myshopname.myshopify.com×tamp=1520883022"
Parse the querystring
params = urllib.parse.parse_qs(qs)
Extract the hmac value
value = params['hmac'][0]
Remove parameters from the querystring per documentation
del params['hmac']
del params['signature']
Recombine the parameters
new_qs = urllib.parse.urlencode(params)
Calculate the digest
h = hmac.new(SECRET.encode("utf8"), msg=new_qs.encode("utf8"), digestmod=hashlib.sha256)
Returns False
!
hmac.compare_digest(h.hexdigest(), value)
That last step should, ostensibly, return true. Every step followed here is outlined as commented in the Shopify docs.
At some point, recently, Shopify started including the protocol
parameter in the querystring payload. This itself wouldn’t be a problem, except for the fact that Shopify doesn’t document that :
and /
are not to be URL-encoded when checking the signature. This is unexpected, given that they themselves do URL-encode these characters in the query string that is provided.
To fix the issue, provide the safe
parameter to urllib.parse.urlencode
with the value :/
(fitting, right?). The full working code looks like this:
params = urllib.parse.parse_qsl(qs)
cleaned_params = []
hmac_value = dict(params)['hmac']
# Sort parameters
for (k, v) in sorted(params):
if k in ['hmac', 'signature']:
continue
cleaned_params.append((k, v))
new_qs = urllib.parse.urlencode(cleaned_params, safe=":/")
secret = SECRET.encode("utf8")
h = hmac.new(secret, msg=new_qs.encode("utf8"), digestmod=hashlib.sha256)
# Compare digests
hmac.compare_digest(h.hexdigest(), hmac_value)
Hope this is helpful for others running into this issue!
import hmac
import hashlib
...
# Inside your view in Django's views.py
params = request.GET.dict()
#
myhmac = params.pop('hmac')
params['state'] = int(params['state'])
line = '&'.join([
'%s=%s' % (key, value)
for key, value in sorted(params.items())
])
print(line)
h = hmac.new(
key=SHARED_SECRET.encode('utf-8'),
msg=line.encode('utf-8'),
digestmod=hashlib.sha256
)
# Cinderella ?
print(hmac.compare_digest(h.hexdigest(), myhmac))