Skip to content

Commit 47b9c38

Browse files
fix(webhooks): parseSqs/ParseSns decode-only; HTTP verify via verifyAndParseWebhook; docs + tests
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 38bd50c commit 47b9c38

4 files changed

Lines changed: 90 additions & 294 deletions

File tree

docs/webhooks/webhooks_overview/webhooks_overview.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -143,46 +143,40 @@ The original `client.verify_webhook(request.body, request.headers["X-Signature"]
143143

144144
For events delivered through SQS or SNS, call the matching helper. It base64-decodes the envelope, gzip-decompresses when the magic bytes are present, and returns the parsed event.
145145

146-
Stream does **not** ship an `X-Signature` on SQS or SNS deliveries: those transports run on AWS-internal infrastructure that is already authenticated end-to-end. SQS queues are reached via IAM-authenticated polling, and SNS notifications carry an AWS signature on the notification envelope itself, so verifying that the message really came from your topic happens at the AWS layer. Layering an HMAC check on top is redundant, so `signature` and `secret` are optional for the SQS/SNS helpers. If you want the legacy verification pipeline you can still pass both — but you do not need to.
146+
Stream does **not** ship an `X-Signature` on SQS or SNS deliveries: those transports run on AWS-internal infrastructure that is already authenticated end-to-end. SQS queues are reached via IAM-authenticated polling, and SNS notifications carry an AWS signature on the notification envelope itself, so verifying that the message really came from your topic happens at the AWS layer. Layering an HMAC check on top is redundant, so the SQS/SNS helpers only decode and parse — they take a single argument and never verify a signature.
147147

148148
For SQS, pass the message `Body` (already the payload):
149149

150150
```python
151-
event = client.verify_and_parse_sqs(sqs_message["Body"])
151+
event = client.parse_sqs(sqs_message["Body"])
152152
```
153153

154154
For SNS, pass the **raw notification body** (the full `{"Type":"Notification", ...}` JSON envelope Amazon delivers). The SDK extracts the inner `Message` field for you, so the call site mirrors what HTTP frameworks already hand you in `request.body`:
155155

156156
```python
157157
# Django SNS HTTP delivery
158-
event = client.verify_and_parse_sns(request.body) # raw envelope (bytes/str)
158+
event = client.parse_sns(request.body) # raw envelope (bytes/str)
159159
```
160160

161161
#### Stateless / module-level form
162162

163-
If you do not want to construct a `StreamChat` client (for example in a lightweight Lambda that only handles webhooks), call the module-level helpers directly. The HTTP helper still requires the signature and secret; the SQS/SNS helpers take them as optional positional arguments:
163+
If you do not want to construct a `StreamChat` client (for example in a lightweight Lambda that only handles webhooks), call the module-level helpers directly. The HTTP helper still requires the signature and secret; the SQS/SNS helpers take a single argument:
164164

165165
```python
166166
from stream_chat import webhook
167167

168168
event = webhook.verify_and_parse_webhook(body, signature, secret)
169-
event = webhook.verify_and_parse_sqs(message_body)
170-
event = webhook.verify_and_parse_sns(notification_body)
171-
172-
# Opt-in HMAC verification for SQS / SNS (defence in depth)
173-
event = webhook.verify_and_parse_sqs(message_body, signature, secret)
174-
event = webhook.verify_and_parse_sns(notification_body, signature, secret)
169+
event = webhook.parse_sqs(message_body)
170+
event = webhook.parse_sns(notification_body)
175171
```
176172

177-
Passing only one of `signature` / `secret` to the SQS or SNS helper is a programmer error and raises `InvalidWebhookError("signature and secret must both be provided to verify the SQS/SNS payload")`.
178-
179173
##### Arguments
180174

181-
| Argument | `verify_and_parse_webhook` | `verify_and_parse_sqs` | `verify_and_parse_sns` |
182-
| ------------------- | -------------------------- | ---------------------- | ---------------------- |
175+
| Argument | `verify_and_parse_webhook` | `parse_sqs` | `parse_sns` |
176+
| ------------------- | -------------------------- | --------------- | -------------------- |
183177
| body / message_body / notification_body | required | required | required |
184-
| signature | required | optional | optional |
185-
| secret | required | optional | optional |
178+
| signature | required | | |
179+
| secret | required | | |
186180

187181
The module also exposes the primitives the composites are built from — `gunzip_payload`, `decode_sqs_payload`, `decode_sns_payload`, `verify_signature` (constant-time HMAC-SHA256), and `parse_event` — for callers that need to run the steps individually.
188182

stream_chat/base/client.py

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -147,68 +147,26 @@ def verify_and_parse_webhook(
147147
148148
:param body: raw HTTP request body bytes Stream signed
149149
:param signature: ``X-Signature`` header value
150-
:raises stream_chat.webhook.InvalidWebhookError: on
150+
:raises stream_chat.base.exceptions.WebhookSignatureError: on
151151
signature mismatch or any decode error
152152
"""
153153
from stream_chat.webhook import verify_and_parse_webhook
154154

155155
return verify_and_parse_webhook(body, signature, self.api_secret)
156156

157-
def verify_and_parse_sqs(
158-
self,
159-
message_body: Union[bytes, str],
160-
signature: Optional[Union[str, bytes]] = None,
161-
) -> Dict[str, Any]:
162-
"""Parse an SQS firehose webhook event.
163-
164-
Reverses the base64 (+ optional gzip) wrapping on the SQS
165-
``Body`` and returns the parsed event. Stream does not attach
166-
an ``X-Signature`` to SQS deliveries -- the transport is an
167-
IAM-authenticated AWS queue, so HMAC verification on top is
168-
redundant and signature verification is therefore optional.
169-
When ``signature`` is supplied the app's API secret is used to
170-
run the legacy verification pipeline.
171-
172-
:param message_body: SQS message ``Body`` (string)
173-
:param signature: optional ``X-Signature`` message attribute
174-
value; when supplied, signature verification runs
175-
:raises stream_chat.webhook.InvalidWebhookError: on
176-
signature mismatch or any decode error
177-
"""
178-
from stream_chat.webhook import verify_and_parse_sqs
157+
def parse_sqs(self, message_body: Union[bytes, str]) -> Dict[str, Any]:
158+
"""Parse an SQS firehose body (base64 + optional gzip). No HMAC."""
179159

180-
if signature is None:
181-
return verify_and_parse_sqs(message_body)
182-
return verify_and_parse_sqs(message_body, signature, self.api_secret)
160+
from stream_chat.webhook import parse_sqs
183161

184-
def verify_and_parse_sns(
185-
self,
186-
notification_body: Union[bytes, str],
187-
signature: Optional[Union[str, bytes]] = None,
188-
) -> Dict[str, Any]:
189-
"""Parse an SNS firehose webhook event.
190-
191-
Reverses the base64 (+ optional gzip) wrapping on the SNS
192-
``Message`` and returns the parsed event. Stream does not
193-
attach an ``X-Signature`` to SNS deliveries -- AWS already
194-
signs the SNS notification envelope, so HMAC verification on
195-
top is redundant and signature verification is therefore
196-
optional. When ``signature`` is supplied the app's API secret
197-
is used to run the legacy verification pipeline.
198-
199-
:param notification_body: raw SNS notification body (the full
200-
``{"Type":"Notification", ...}`` JSON envelope, or a
201-
pre-extracted ``Message`` string)
202-
:param signature: optional ``X-Signature`` message attribute
203-
value; when supplied, signature verification runs
204-
:raises stream_chat.webhook.InvalidWebhookError: on
205-
signature mismatch or any decode error
206-
"""
207-
from stream_chat.webhook import verify_and_parse_sns
162+
return parse_sqs(message_body)
163+
164+
def parse_sns(self, message: Union[bytes, str]) -> Dict[str, Any]:
165+
"""Parse an SNS body (unwraps SNS envelope when present). No HMAC."""
166+
167+
from stream_chat.webhook import parse_sns
208168

209-
if signature is None:
210-
return verify_and_parse_sns(notification_body)
211-
return verify_and_parse_sns(notification_body, signature, self.api_secret)
169+
return parse_sns(message)
212170

213171
@abc.abstractmethod
214172
def update_app_settings(

0 commit comments

Comments
 (0)