Skip to content

Commit de2ca57

Browse files
authored
Hmac validation bank webhooks (#302)
* Add is_valid_hmac_payload function * Add unit test
1 parent 0cfa158 commit de2ca57

File tree

2 files changed

+94
-1
lines changed

2 files changed

+94
-1
lines changed

Adyen/util.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import copy
88

99

10-
10+
# generates HMAC signature for the NotificationRequest object
1111
def generate_notification_sig(dict_object, hmac_key):
1212

1313
if not isinstance(dict_object, dict):
@@ -36,7 +36,30 @@ def generate_notification_sig(dict_object, hmac_key):
3636
return base64.b64encode(hm.digest())
3737

3838

39+
# generates HMAC signature for the payload (bytes)
40+
def generate_payload_sig(payload, hmac_key):
41+
42+
if not isinstance(payload, bytes):
43+
raise ValueError("Must Provide payload as bytes")
44+
45+
hmac_key = binascii.a2b_hex(hmac_key)
46+
47+
hm = hmac.new(hmac_key, payload, hashlib.sha256)
48+
return base64.b64encode(hm.digest())
49+
50+
3951
def is_valid_hmac_notification(dict_object, hmac_key):
52+
"""
53+
validates the HMAC signature of the NotificationRequestItem object. Use for webhooks that provide the
54+
hmacSignature as part of the payload `AdditionalData` (i.e. Payments)
55+
Args:
56+
dict_object: object with a list of notificationItems
57+
hmac_key: HMAC key to generate the signature
58+
59+
Returns:
60+
boolean: true when HMAC signature is valid
61+
"""
62+
4063
dict_object = copy.deepcopy(dict_object)
4164

4265
if 'notificationItems' in dict_object:
@@ -53,5 +76,24 @@ def is_valid_hmac_notification(dict_object, hmac_key):
5376
return hmac.compare_digest(merchant_sign_str, expected_sign)
5477

5578

79+
def is_valid_hmac_payload(hmac_signature, hmac_key, payload):
80+
"""
81+
validates the HMAC signature of a payload against an expected signature. Use for webhooks that provide the
82+
hmacSignature in the HTTP header (i.e. Banking, Management API)
83+
Args:
84+
hmac_signature: HMAC signature to validate
85+
hmac_key: HMAC key to generate the signature
86+
payload: webhook payload
87+
88+
Returns:
89+
boolean: true when HMAC signature is valid
90+
"""
91+
92+
merchant_sign = generate_payload_sig(payload, hmac_key)
93+
merchant_sign_str = merchant_sign.decode("utf-8")
94+
95+
return hmac.compare_digest(merchant_sign_str, hmac_signature)
96+
97+
5698
def get_query(query_parameters):
5799
return '?' + '&'.join(["{}={}".format(k, v) for k, v in query_parameters.items()])

test/UtilTest.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from Adyen import settings
55
from Adyen.util import (
66
generate_notification_sig,
7+
is_valid_hmac_payload,
78
is_valid_hmac_notification,
89
get_query
910
)
@@ -132,3 +133,53 @@ def test_is_valid_hmac_notification_removes_additional_data(self):
132133
]}
133134
is_valid_hmac_notification(notification, "11aa")
134135
self.assertIsNotNone(notification['notificationItems'][0]['NotificationRequestItem']['additionalData'])
136+
137+
def test_is_valid_hmac_payload(self):
138+
139+
payload = '''
140+
{
141+
"type": "merchant.created",
142+
"environment": "test",
143+
"createdAt": "01-01-2024",
144+
"data": {
145+
"capabilities": {
146+
"sendToTransferInstrument": {
147+
"requested": true,
148+
"requestedLevel": "notApplicable"
149+
}
150+
},
151+
"companyId": "YOUR_COMPANY_ID",
152+
"merchantId": "YOUR_MERCHANT_ACCOUNT",
153+
"status": "PreActive"
154+
}
155+
}
156+
'''
157+
hmac_key = "44782DEF547AAA06C910C43932B1EB0C71FC68D9D0C057550C48EC2ACF6BA056"
158+
expected_hmac = "fX74xUdztFmaXAn3IusMFFUBUSkLmDQUK0tm8xL6ZTU="
159+
160+
self.assertTrue(is_valid_hmac_payload(expected_hmac, hmac_key, payload.encode("utf-8")))
161+
162+
def test_is_invalid_hmac_payload(self):
163+
payload = '''
164+
{
165+
"type": "merchant.created",
166+
"environment": "test",
167+
"createdAt": "01-01-2024",
168+
"data": {
169+
"capabilities": {
170+
"sendToTransferInstrument": {
171+
"requested": true,
172+
"requestedLevel": "notApplicable"
173+
}
174+
},
175+
"companyId": "YOUR_COMPANY_ID",
176+
"merchantId": "YOUR_MERCHANT_ACCOUNT",
177+
"status": "PreActive"
178+
}
179+
}
180+
'''
181+
182+
hmac_key = "44782DEF547AAA06C910C43932B1EB0C71FC68D9D0C057550C48EC2ACF6BA056"
183+
expected_hmac = "MismatchingHmacKey="
184+
185+
self.assertFalse(is_valid_hmac_payload(expected_hmac, hmac_key, payload.encode("utf-8")))

0 commit comments

Comments
 (0)