Skip to content

Commit e157ec7

Browse files
committed
Additional improvements for slackapi#686 and document updates
1 parent ce75384 commit e157ec7

File tree

4 files changed

+44
-70
lines changed

4 files changed

+44
-70
lines changed

docs-src/basic_usage.rst

+5-8
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,16 @@ Modals use the same blocks that compose messages with the addition of an `input`
178178

179179
.. code-block:: python
180180
181+
# This module is available since v2.6.0rc1
182+
from slack.signature import SignatureVerifier
183+
signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"])
184+
181185
from flask import Flask, request, make_response
182186
app = Flask(__name__)
183-
signing_secret = os.environ["SLACK_SIGNING_SECRET"]
184187
185188
@app.route("/slack/events", methods=["POST"])
186189
def slack_app():
187-
# Refer to https://github.com/slackapi/python-slack-events-api
188-
# (The Slack Team is going to provide a new package soon)
189-
if not verify_request(
190-
signing_secret=signing_secret,
191-
request_body=request.get_data(),
192-
timestamp=request.headers.get("X-Slack-Request-Timestamp"),
193-
signature=request.headers.get("X-Slack-Signature")):
190+
if not signature_verifier.is_valid_request(request.get_data(), request.headers):
194191
return make_response("invalid request", 403)
195192
196193
if "command" in request.form \

integration_tests/samples/basic_usage/views.py

+3-52
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,6 @@
77

88
sys.path.insert(1, f"{dirname(__file__)}/../../..")
99
logging.basicConfig(level=logging.DEBUG)
10-
# ------------------
11-
12-
# ---------------------
13-
# Slack Request Verification
14-
# https://github.com/slackapi/python-slack-events-api
15-
# ---------------------
16-
17-
import hmac
18-
import hashlib
19-
from time import time
20-
21-
22-
def verify_request(
23-
signing_secret: str,
24-
request_body: str,
25-
timestamp: str,
26-
signature: str) -> bool:
27-
if abs(time() - int(timestamp)) > 60 * 5:
28-
return False
29-
30-
if hasattr(hmac, "compare_digest"):
31-
req = str.encode('v0:' + str(timestamp) + ':') + request_body
32-
request_hash = 'v0=' + hmac.new(
33-
str.encode(signing_secret),
34-
req, hashlib.sha256
35-
).hexdigest()
36-
return hmac.compare_digest(request_hash, signature)
37-
else:
38-
# So, we'll compare the signatures explicitly
39-
req = str.encode('v0:' + str(timestamp) + ':') + request_body
40-
request_hash = 'v0=' + hmac.new(
41-
str.encode(signing_secret),
42-
req, hashlib.sha256
43-
).hexdigest()
44-
45-
if len(request_hash) != len(signature):
46-
return False
47-
result = 0
48-
if isinstance(request_hash, bytes) and isinstance(signature, bytes):
49-
for x, y in zip(request_hash, signature):
50-
result |= x ^ y
51-
else:
52-
for x, y in zip(request_hash, signature):
53-
result |= ord(x) ^ ord(y)
54-
return result == 0
55-
5610

5711
# ---------------------
5812
# Slack WebClient
@@ -62,8 +16,10 @@ def verify_request(
6216

6317
from slack import WebClient
6418
from slack.errors import SlackApiError
19+
from slack.signature import SignatureVerifier
6520

6621
client = WebClient(token=os.environ["SLACK_API_TOKEN"])
22+
signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"])
6723

6824
# ---------------------
6925
# Flask App
@@ -73,16 +29,11 @@ def verify_request(
7329
from flask import Flask, request, make_response
7430

7531
app = Flask(__name__)
76-
signing_secret = os.environ["SLACK_SIGNING_SECRET"]
7732

7833

7934
@app.route("/slack/events", methods=["POST"])
8035
def slack_app():
81-
if not verify_request(
82-
signing_secret=signing_secret,
83-
request_body=request.get_data(),
84-
timestamp=request.headers.get("X-Slack-Request-Timestamp"),
85-
signature=request.headers.get("X-Slack-Signature")):
36+
if not signature_verifier.is_valid_request(request.get_data(), request.headers):
8637
return make_response("invalid request", 403)
8738

8839
if "command" in request.form \

slack/signature/verifier.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import hashlib
22
import hmac
33
from time import time
4-
from typing import Dict, Optional
4+
from typing import Dict, Optional, Union
55

66

77
class Clock:
@@ -21,7 +21,9 @@ def __init__(self, signing_secret: str, clock: Clock = Clock()):
2121
self.signing_secret = signing_secret
2222
self.clock = clock
2323

24-
def is_valid_request(self, body: str, headers: Dict[str, str],) -> bool:
24+
def is_valid_request(
25+
self, body: Union[str, bytes], headers: Dict[str, str],
26+
) -> bool:
2527
"""Verifies if the given signature is valid"""
2628
if headers is None:
2729
return False
@@ -32,26 +34,31 @@ def is_valid_request(self, body: str, headers: Dict[str, str],) -> bool:
3234
signature=normalized_headers.get("x-slack-signature", None),
3335
)
3436

35-
def is_valid(self, body: str, timestamp: str, signature: str,) -> bool:
37+
def is_valid(
38+
self, body: Union[str, bytes], timestamp: str, signature: str,
39+
) -> bool:
3640
"""Verifies if the given signature is valid"""
3741
if timestamp is None or signature is None:
3842
return False
3943

4044
if abs(self.clock.now() - int(timestamp)) > 60 * 5:
4145
return False
4246

43-
if body is None:
44-
body = ""
45-
4647
calculated_signature = self.generate_signature(timestamp=timestamp, body=body)
4748
if calculated_signature is None:
4849
return False
4950
return hmac.compare_digest(calculated_signature, signature)
5051

51-
def generate_signature(self, *, timestamp: str, body: str) -> Optional[str]:
52+
def generate_signature(
53+
self, *, timestamp: str, body: Union[str, bytes]
54+
) -> Optional[str]:
5255
"""Generates a signature"""
5356
if timestamp is None:
5457
return None
58+
if body is None:
59+
body = ""
60+
if isinstance(body, bytes):
61+
body = body.decode("utf-8")
5562

5663
format_req = str.encode(f"v0:{timestamp}:{body}")
5764
encoded_secret = str.encode(self.signing_secret)

tests/signature/test_signature_verifier.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ def tearDown(self):
2929
}
3030

3131
def test_generate_signature(self):
32-
verifier = SignatureVerifier("8f742231b10e8888abcd99yyyzzz85a5")
33-
timestamp = "1531420618"
34-
signature = verifier.generate_signature(timestamp=timestamp, body=self.body)
32+
verifier = SignatureVerifier(self.signing_secret)
33+
signature = verifier.generate_signature(timestamp=self.timestamp, body=self.body)
34+
self.assertEqual(self.valid_signature, signature)
35+
36+
def test_generate_signature_body_as_bytes(self):
37+
verifier = SignatureVerifier(self.signing_secret)
38+
signature = verifier.generate_signature(timestamp=self.timestamp, body=self.body.encode("utf-8"))
3539
self.assertEqual(self.valid_signature, signature)
3640

3741
def test_is_valid_request(self):
@@ -41,6 +45,13 @@ def test_is_valid_request(self):
4145
)
4246
self.assertTrue(verifier.is_valid_request(self.body, self.headers))
4347

48+
def test_is_valid_request_body_as_bytes(self):
49+
verifier = SignatureVerifier(
50+
signing_secret=self.signing_secret,
51+
clock=MockClock()
52+
)
53+
self.assertTrue(verifier.is_valid_request(self.body.encode("utf-8"), self.headers))
54+
4455
def test_is_valid_request_invalid_body(self):
4556
verifier = SignatureVerifier(
4657
signing_secret=self.signing_secret,
@@ -49,6 +60,14 @@ def test_is_valid_request_invalid_body(self):
4960
modified_body = self.body + "------"
5061
self.assertFalse(verifier.is_valid_request(modified_body, self.headers))
5162

63+
def test_is_valid_request_invalid_bodyas_bytes(self):
64+
verifier = SignatureVerifier(
65+
signing_secret=self.signing_secret,
66+
clock=MockClock(),
67+
)
68+
modified_body = self.body + "------"
69+
self.assertFalse(verifier.is_valid_request(modified_body.encode("utf-8"), self.headers))
70+
5271
def test_is_valid_request_expiration(self):
5372
verifier = SignatureVerifier(
5473
signing_secret=self.signing_secret,

0 commit comments

Comments
 (0)