Skip to content

Commit 03586fe

Browse files
authored
Merge pull request #686 from seratch/signature-verifier
Add SignatureVerifier for request verification
2 parents 58134fe + c352e88 commit 03586fe

File tree

4 files changed

+155
-4
lines changed

4 files changed

+155
-4
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

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import hashlib
2+
import hmac
3+
from time import time
4+
from typing import Dict, Optional
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+
if headers is None:
27+
return False
28+
normalized_headers = {k.lower(): v for k, v in headers.items()}
29+
return self.is_valid(
30+
body=body,
31+
timestamp=normalized_headers.get("x-slack-request-timestamp", None),
32+
signature=normalized_headers.get("x-slack-signature", None),
33+
)
34+
35+
def is_valid(self, body: str, timestamp: str, signature: str,) -> bool:
36+
"""Verifies if the given signature is valid"""
37+
if timestamp is None or signature is None:
38+
return False
39+
40+
if abs(self.clock.now() - int(timestamp)) > 60 * 5:
41+
return False
42+
43+
if body is None:
44+
body = ""
45+
46+
calculated_signature = self.generate_signature(timestamp=timestamp, body=body)
47+
if calculated_signature is None:
48+
return False
49+
return hmac.compare_digest(calculated_signature, signature)
50+
51+
def generate_signature(self, *, timestamp: str, body: str) -> Optional[str]:
52+
"""Generates a signature"""
53+
if timestamp is None:
54+
return None
55+
56+
format_req = str.encode(f"v0:{timestamp}:{body}")
57+
encoded_secret = str.encode(self.signing_secret)
58+
request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
59+
calculated_signature = f"v0={request_hash}"
60+
return calculated_signature

slack/web/base_client.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import hmac
77
import io
88
import json
9-
import json as json_module
109
import logging
1110
import mimetypes
1211
import os
@@ -15,8 +14,8 @@
1514
import uuid
1615
import warnings
1716
from http.client import HTTPResponse
18-
from typing import BinaryIO, Dict, List, Union
19-
from typing import Optional
17+
from typing import BinaryIO, Dict, List
18+
from typing import Optional, Union
2019
from urllib.error import HTTPError
2120
from urllib.parse import urlencode
2221
from urllib.parse import urljoin
@@ -333,7 +332,7 @@ def _request_for_pagination(self, api_url, req_args) -> Dict[str, any]:
333332
return {
334333
"status_code": int(response["status"]),
335334
"headers": dict(response["headers"]),
336-
"data": json_module.loads(response["body"]),
335+
"data": json.loads(response["body"]),
337336
}
338337

339338
def _urllib_api_call(
@@ -613,6 +612,11 @@ def validate_slack_signature(
613612
Returns:
614613
True if signatures matches
615614
"""
615+
warnings.warn(
616+
"As this method is deprecated since slackclient 2.6.0, "
617+
"use `from slack.signature import SignatureVerifier` instead",
618+
DeprecationWarning,
619+
)
616620
format_req = str.encode(f"v0:{timestamp}:{data}")
617621
encoded_secret = str.encode(signing_secret)
618622
request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
# https://api.slack.com/authentication/verifying-requests-from-slack
19+
signing_secret = "8f742231b10e8888abcd99yyyzzz85a5"
20+
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+
23+
timestamp = "1531420618"
24+
valid_signature = "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503"
25+
26+
headers = {
27+
"X-Slack-Request-Timestamp": timestamp,
28+
"X-Slack-Signature": valid_signature,
29+
}
30+
31+
def test_generate_signature(self):
32+
verifier = SignatureVerifier("8f742231b10e8888abcd99yyyzzz85a5")
33+
timestamp = "1531420618"
34+
signature = verifier.generate_signature(timestamp=timestamp, body=self.body)
35+
self.assertEqual(self.valid_signature, signature)
36+
37+
def test_is_valid_request(self):
38+
verifier = SignatureVerifier(
39+
signing_secret=self.signing_secret,
40+
clock=MockClock()
41+
)
42+
self.assertTrue(verifier.is_valid_request(self.body, self.headers))
43+
44+
def test_is_valid_request_invalid_body(self):
45+
verifier = SignatureVerifier(
46+
signing_secret=self.signing_secret,
47+
clock=MockClock(),
48+
)
49+
modified_body = self.body + "------"
50+
self.assertFalse(verifier.is_valid_request(modified_body, self.headers))
51+
52+
def test_is_valid_request_expiration(self):
53+
verifier = SignatureVerifier(
54+
signing_secret=self.signing_secret,
55+
)
56+
self.assertFalse(verifier.is_valid_request(self.body, self.headers))
57+
58+
def test_is_valid_request_none(self):
59+
verifier = SignatureVerifier(
60+
signing_secret=self.signing_secret,
61+
clock=MockClock(),
62+
)
63+
self.assertFalse(verifier.is_valid_request(None, self.headers))
64+
self.assertFalse(verifier.is_valid_request(self.body, None))
65+
self.assertFalse(verifier.is_valid_request(None, None))
66+
67+
def test_is_valid(self):
68+
verifier = SignatureVerifier(
69+
signing_secret=self.signing_secret,
70+
clock=MockClock(),
71+
)
72+
self.assertTrue(verifier.is_valid(self.body, self.timestamp, self.valid_signature))
73+
self.assertTrue(verifier.is_valid(self.body, 1531420618, self.valid_signature))
74+
75+
def test_is_valid_none(self):
76+
verifier = SignatureVerifier(
77+
signing_secret=self.signing_secret,
78+
clock=MockClock(),
79+
)
80+
self.assertFalse(verifier.is_valid(None, self.timestamp, self.valid_signature))
81+
self.assertFalse(verifier.is_valid(self.body, None, self.valid_signature))
82+
self.assertFalse(verifier.is_valid(self.body, self.timestamp, None))
83+
self.assertFalse(verifier.is_valid(None, None, self.valid_signature))
84+
self.assertFalse(verifier.is_valid(None, self.timestamp, None))
85+
self.assertFalse(verifier.is_valid(self.body, None, None))
86+
self.assertFalse(verifier.is_valid(None, None, None))

0 commit comments

Comments
 (0)