Skip to content

Commit 67827ff

Browse files
author
Will Myers
authored
Add #is_webhook_authentic (#10)
* Add #is_webhook_authentic * Update README.rst * Fix python 3.x * More updates for python 3 * Remove u'' prefix (not supported in python 3.2) * Update utils_test.py * Pull verison back down to 2.0.0
1 parent cba3e8e commit 67827ff

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

README.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,30 @@ supply for the next page of results. If ``prev_cursor()`` returns
275275
# <class pybutton.Response [100 elements]>
276276
277277
278+
Utils
279+
---------
280+
281+
Utils houses generic helpers useful in a Button Integration.
282+
283+
#is_webhook_authentic
284+
~~~~~~~~~~~~~~~~~~~
285+
286+
Used to verify that requests sent to a webhook endpoint are from Button and that
287+
their payload can be trusted. Returns ``True`` if a webhook request body matches
288+
the sent signature and ``False`` otherwise. See `Webhook Security <https://www.usebutton.com/developers/webhooks/#security>`__ for more details.
289+
290+
.. code:: python
291+
292+
import os
293+
294+
from pybutton.utils import is_webhook_authentic
295+
296+
is_webhook_authentic(
297+
os.environ['WEBHOOK_SECRET'],
298+
request.data,
299+
request.headers.get('X-Button-Signature')
300+
)
301+
278302
Contributing
279303
------------
280304

pybutton/utils.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import absolute_import
2+
from __future__ import division
3+
from __future__ import print_function
4+
from __future__ import unicode_literals
5+
6+
import sys
7+
import hmac
8+
import hashlib
9+
10+
11+
def is_webhook_authentic(webhook_secret, request_body, sent_signature):
12+
'''Used to verify that requests sent to a webhook endpoint are from Button
13+
and that their payload can be trusted. Returns True if a webhook request
14+
body matches the sent signature and False otherwise.
15+
16+
Args:
17+
webhook_secret (basestring): Your webhooks's secret key. Find yours at
18+
https://app.usebutton.com/webhooks.
19+
20+
request_body (basestring): UTF8 encoded byte-string of the request body
21+
22+
sent_signature (basestring): "X-Button-Siganture" HTTP Header sent with
23+
the request.
24+
25+
Returns:
26+
(bool) Whether or not the request is authentic
27+
'''
28+
29+
computed_signature = hmac.new(
30+
as_bytes(webhook_secret),
31+
as_bytes(request_body),
32+
hashlib.sha256
33+
).hexdigest()
34+
35+
if hasattr(hmac, 'compare_digest'):
36+
return hmac.compare_digest(
37+
computed_signature,
38+
as_bytes(sent_signature, True)
39+
)
40+
41+
return computed_signature == sent_signature
42+
43+
44+
def as_bytes(v, only_py_2=False):
45+
'''Converts v to a UTF-8 byte string if unicode, else returns identity.
46+
47+
Args:
48+
v (str|unicode): the string to convert
49+
50+
only_py_2 (bool): If true, only converts to bytes if running in a
51+
python 2 interpretter
52+
53+
Returns:
54+
(byte string): A byte string copy, UTF-8 enccoded
55+
'''
56+
57+
python_version = sys.version_info[0]
58+
59+
if only_py_2 and python_version != 2:
60+
return v
61+
62+
should_encode = (
63+
python_version == 2 and isinstance(v, unicode)
64+
or python_version == 3 and isinstance(v, str)
65+
)
66+
67+
if should_encode:
68+
return v.encode('utf8')
69+
70+
return v

test/utils_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import absolute_import
2+
from __future__ import division
3+
from __future__ import print_function
4+
from __future__ import unicode_literals
5+
6+
from unittest import TestCase
7+
8+
from pybutton.utils import is_webhook_authentic
9+
10+
11+
class UtilsTestCase(TestCase):
12+
13+
def test_is_webhook_authentic(self):
14+
signature = (
15+
'79a3a5291c94340ff0058a6319063757'
16+
'68d706357ee86826c3c692e6b9aa6817'
17+
)
18+
payload = '{ "a": 1 }'
19+
20+
self.assertFalse(is_webhook_authentic('secret', payload, 'XXX'))
21+
self.assertTrue(is_webhook_authentic('secret', payload, signature))
22+
self.assertFalse(is_webhook_authentic('secret?', payload, signature))
23+
self.assertFalse(is_webhook_authentic(
24+
'secret', '{ "a": 2 }', signature)
25+
)
26+
27+
def test_is_webhook_authentic_unicode_payload(self):
28+
signature = (
29+
'3040cf48ab225ca539c1d23841175bc2'
30+
'2e565cdb0975bd690ecaeca2c39dfcf7'
31+
)
32+
33+
self.assertTrue(
34+
is_webhook_authentic('secret', '{ "a": \u1f60e }', signature)
35+
)
36+
37+
def test_is_webhook_authentic_byte_strings(self):
38+
signature = (
39+
'79a3a5291c94340ff0058a6319063757'
40+
'68d706357ee86826c3c692e6b9aa6817'
41+
)
42+
payload = b'{ "a": 1 }'
43+
44+
self.assertFalse(is_webhook_authentic(b'secret', payload, 'XXX'))
45+
self.assertTrue(is_webhook_authentic(b'secret', payload, signature))
46+
self.assertFalse(is_webhook_authentic(b'secret?', payload, signature))
47+
self.assertFalse(
48+
is_webhook_authentic(b'secret', b'{ "a": 2 }', signature)
49+
)

0 commit comments

Comments
 (0)