Skip to content

Commit 1162494

Browse files
committed
Add utility functions for webhooks
1 parent d59b7a3 commit 1162494

File tree

3 files changed

+92
-1
lines changed

3 files changed

+92
-1
lines changed

flagsmith/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from . import webhooks
12
from .flagsmith import Flagsmith
23

3-
__all__ = ("Flagsmith",)
4+
__all__ = ("Flagsmith", "webhooks")

flagsmith/webhooks.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import hashlib
2+
import hmac
3+
4+
5+
def generate_signature(
6+
request_body: str | bytes,
7+
shared_secret: str,
8+
) -> str:
9+
"""Generates a signature for a webhook request body using HMAC-SHA256.
10+
11+
:param request_body: The raw request body, as string or bytes.
12+
:param shared_secret: The shared secret configured for this specific webhook.
13+
:return: The hex-encoded signature.
14+
"""
15+
if isinstance(request_body, str):
16+
request_body = request_body.encode()
17+
18+
shared_secret_bytes = shared_secret.encode()
19+
20+
return hmac.new(
21+
key=shared_secret_bytes,
22+
msg=request_body,
23+
digestmod=hashlib.sha256,
24+
).hexdigest()
25+
26+
27+
def verify_signature(
28+
request_body: str | bytes,
29+
received_signature: str,
30+
shared_secret: str,
31+
) -> bool:
32+
"""Verifies a webhook's signature to determine if the request was sent by Flagsmith.
33+
34+
:param request_body: The raw request body, as string or bytes.
35+
:param received_signature: The signature as received in the X-Flagsmith-Signature request header.
36+
:param shared_secret: The shared secret configured for this specific webhook.
37+
:return: True if the signature is valid, False otherwise.
38+
"""
39+
expected_signature = generate_signature(request_body, shared_secret)
40+
return hmac.compare_digest(expected_signature, received_signature)

tests/test_webhooks.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import json
2+
3+
from flagsmith.webhooks import generate_signature, verify_signature
4+
5+
6+
def test_generate_signature():
7+
# Given
8+
request_body = json.dumps({"data": {"foo": 123}})
9+
shared_secret = "shh"
10+
11+
# When
12+
signature = generate_signature(request_body, shared_secret)
13+
14+
# Then
15+
assert isinstance(signature, str)
16+
assert len(signature) == 64 # SHA-256 hex digest is 64 characters
17+
18+
19+
def test_verify_signature_valid():
20+
# Given
21+
request_body = json.dumps({"data": {"foo": 123}})
22+
shared_secret = "shh"
23+
24+
# When
25+
signature = generate_signature(request_body, shared_secret)
26+
27+
# Then
28+
assert verify_signature(
29+
request_body=request_body,
30+
received_signature=signature,
31+
shared_secret=shared_secret,
32+
)
33+
# Test with bytes instead of str
34+
assert verify_signature(
35+
request_body=request_body.encode(),
36+
received_signature=signature,
37+
shared_secret=shared_secret,
38+
)
39+
40+
41+
def test_verify_signature_invalid():
42+
# Given
43+
request_body = json.dumps({"event": "flag_updated", "data": {"id": 123}})
44+
45+
# Then
46+
assert not verify_signature(
47+
request_body=request_body,
48+
received_signature="bad",
49+
shared_secret="?",
50+
)

0 commit comments

Comments
 (0)