-
Notifications
You must be signed in to change notification settings - Fork 11
feat: Add utility functions for webhooks #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| from . import webhooks | ||
| from .flagsmith import Flagsmith | ||
|
|
||
| __all__ = ("Flagsmith",) | ||
| __all__ = ("Flagsmith", "webhooks") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import hashlib | ||
| import hmac | ||
| from typing import Union | ||
|
|
||
|
|
||
| def generate_signature( | ||
| request_body: Union[str, bytes], | ||
| shared_secret: str, | ||
| ) -> str: | ||
| """Generates a signature for a webhook request body using HMAC-SHA256. | ||
|
|
||
| :param request_body: The raw request body, as string or bytes. | ||
| :param shared_secret: The shared secret configured for this specific webhook. | ||
| :return: The hex-encoded signature. | ||
| """ | ||
| if isinstance(request_body, str): | ||
| request_body = request_body.encode() | ||
|
|
||
| shared_secret_bytes = shared_secret.encode() | ||
|
|
||
| return hmac.new( | ||
| key=shared_secret_bytes, | ||
| msg=request_body, | ||
| digestmod=hashlib.sha256, | ||
| ).hexdigest() | ||
|
|
||
|
|
||
| def verify_signature( | ||
| request_body: Union[str, bytes], | ||
| received_signature: str, | ||
| shared_secret: str, | ||
| ) -> bool: | ||
| """Verifies a webhook's signature to determine if the request was sent by Flagsmith. | ||
|
|
||
| :param request_body: The raw request body, as string or bytes. | ||
| :param received_signature: The signature as received in the X-Flagsmith-Signature request header. | ||
| :param shared_secret: The shared secret configured for this specific webhook. | ||
| :return: True if the signature is valid, False otherwise. | ||
| """ | ||
| expected_signature = generate_signature(request_body, shared_secret) | ||
| return hmac.compare_digest(expected_signature, received_signature) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import json | ||
|
|
||
| from flagsmith.webhooks import generate_signature, verify_signature | ||
|
|
||
|
|
||
| def test_generate_signature() -> None: | ||
| # Given | ||
| request_body = json.dumps({"data": {"foo": 123}}) | ||
| shared_secret = "shh" | ||
|
|
||
| # When | ||
| signature = generate_signature(request_body, shared_secret) | ||
|
|
||
| # Then | ||
| assert isinstance(signature, str) | ||
| assert len(signature) == 64 # SHA-256 hex digest is 64 characters | ||
|
|
||
|
|
||
| def test_verify_signature_valid() -> None: | ||
| # Given | ||
| request_body = json.dumps({"data": {"foo": 123}}) | ||
| shared_secret = "shh" | ||
|
|
||
| # When | ||
| signature = generate_signature(request_body, shared_secret) | ||
|
|
||
| # Then | ||
| assert verify_signature( | ||
| request_body=request_body, | ||
| received_signature=signature, | ||
| shared_secret=shared_secret, | ||
| ) | ||
| # Test with bytes instead of str | ||
| assert verify_signature( | ||
| request_body=request_body.encode(), | ||
| received_signature=signature, | ||
| shared_secret=shared_secret, | ||
| ) | ||
|
|
||
|
|
||
| def test_verify_signature_invalid() -> None: | ||
| # Given | ||
| request_body = json.dumps({"event": "flag_updated", "data": {"id": 123}}) | ||
|
|
||
| # Then | ||
| assert not verify_signature( | ||
| request_body=request_body, | ||
| received_signature="bad", | ||
| shared_secret="?", | ||
| ) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't see any reason that this function should be part of the public interface for this module?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could help for generating fake requests in case you want to test webhook listeners without depending on Flagsmith, or if for whatever reason you need to invoke a webhook manually. It could also help troubleshooting if
verify_signaturefails and you don't understand why (wrong secret, signature or payload).It's the same as how most JWT libraries have methods for generating and verifying signatures, but the vast majority of users will only be verifying signatures.
In any case I don't feel too strongly about this, I'm happy to remove
generate_signaturefrom the public interface and add it later if we need to. Let me know what you prefer!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, yep, ok, happy to leave it in.