Skip to content

Commit 0143c25

Browse files
committed
Add SignatureVerifier for request verification
1 parent af79b19 commit 0143c25

File tree

4 files changed

+90
-0
lines changed

4 files changed

+90
-0
lines changed

slack/signature/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .verifier import SignatureVerifier # noqa

slack/signature/verifier.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import hashlib
2+
import hmac
3+
from time import time
4+
from typing import Dict
5+
6+
7+
class Clock:
8+
def now(self) -> float:
9+
return time()
10+
11+
12+
class SignatureVerifier:
13+
def __init__(self, signing_secret: str, clock: Clock = Clock()):
14+
"""Slack request signature verifier
15+
16+
Slack signs its requests using a secret that's unique to your app.
17+
With the help of signing secrets, your app can more confidently verify
18+
whether requests from us are authentic.
19+
https://api.slack.com/authentication/verifying-requests-from-slack
20+
"""
21+
self.signing_secret = signing_secret
22+
self.clock = clock
23+
24+
def is_valid_request(self, body: str, headers: Dict[str, str],) -> bool:
25+
"""Verifies if the given signature is valid"""
26+
normalized_headers = {k.lower(): v for k, v in headers.items()}
27+
return self.is_valid(
28+
body=body,
29+
timestamp=normalized_headers.get("x-slack-request-timestamp", None),
30+
signature=normalized_headers.get("x-slack-signature", None),
31+
)
32+
33+
def is_valid(self, body: str, timestamp: str, signature: str,) -> bool:
34+
"""Verifies if the given signature is valid"""
35+
if abs(self.clock.now() - int(timestamp)) > 60 * 5:
36+
return False
37+
38+
calculated_signature = self.generate_signature(timestamp=timestamp, body=body)
39+
return hmac.compare_digest(calculated_signature, signature)
40+
41+
def generate_signature(self, *, timestamp: str, body: str) -> str:
42+
"""Generates a signature"""
43+
format_req = str.encode(f"v0:{timestamp}:{body}")
44+
encoded_secret = str.encode(self.signing_secret)
45+
request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
46+
calculated_signature = f"v0={request_hash}"
47+
return calculated_signature

slack/web/base_client.py

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import platform
88
import sys
9+
import warnings
910
from typing import Optional, Union
1011

1112
# Standard Imports
@@ -317,6 +318,11 @@ def validate_slack_signature(
317318
Returns:
318319
True if signatures matches
319320
"""
321+
warnings.warn(
322+
"As this method is deprecated since slackclient 2.6.0, "
323+
"use `from slack.signature import SignatureVerifier` instead",
324+
DeprecationWarning,
325+
)
320326
format_req = str.encode(f"v0:{timestamp}:{data}")
321327
encoded_secret = str.encode(signing_secret)
322328
request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import unittest
2+
3+
from slack.signature import SignatureVerifier
4+
5+
6+
class MockClock:
7+
def now(self) -> float:
8+
return 1531420618
9+
10+
11+
class TestSignatureVerifier(unittest.TestCase):
12+
def setUp(self):
13+
pass
14+
15+
def tearDown(self):
16+
pass
17+
18+
def test_generate_signature(self):
19+
# https://api.slack.com/authentication/verifying-requests-from-slack
20+
verifier = SignatureVerifier("8f742231b10e8888abcd99yyyzzz85a5")
21+
body = "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c"
22+
timestamp = "1531420618"
23+
signature = verifier.generate_signature(timestamp=timestamp, body=body)
24+
self.assertEqual("v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503", signature)
25+
26+
def test_is_valid_request(self):
27+
verifier = SignatureVerifier(
28+
signing_secret="8f742231b10e8888abcd99yyyzzz85a5",
29+
clock=MockClock()
30+
)
31+
body = "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c"
32+
headers = {
33+
"X-Slack-Request-Timestamp": "1531420618",
34+
"X-Slack-Signature": "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503",
35+
}
36+
self.assertTrue(verifier.is_valid_request(body, headers))

0 commit comments

Comments
 (0)