#5what - Webhook signatures
1 messages · Page 1 of 1 (latest)
Hi there! Reading through your question
Yes, test mode events use signatures, too
The only scheme you should use for live events is v1, that's correct
This might be a helpful resource as you're testing: https://stripe.com/docs/webhooks/quickstart
Though this example doesn't verify signatures manually
Would you be open to using our official libraries instead of doing manual signature verification?
I have been trying to use the official libraries. I am only trying to do so manually for debugging because the official libraries have not been working.
I'll post a code snippet
Here's the code I am using to debug webhook verification, with secrets removed: https://gist.github.com/elijahr/49dc7f2f0a120fdfc9428033bc677bda
Sorry for the delay! I'm getting some help from a colleague
First step: give up entirely on manual verification
if you can't make automatic verification work it's because of something with the data itself. Doing it by hand will never work
I'm guessing from your Gist you got the payload by pulling the JSON payload from an actual webhook. BUT... Stripe uses "stegonography" to encode extra data on the webhook JSON body. They use non-coding extra spaces, line breaks, tabs, etc. This can still be parsed as JSON, but the signature verification needs the non-coding parts - that's why you have to be quite careful to not modify it at all before checking signature. (it also kinda masks the issue - the body parses as JSON just fine, so it looks like it's correct, but the verification fails). This is often caused by using request.body instead of request.rawbody, or by something like Express middleware.
Webhook signatures issues are:
- 90% due to using the wrong secret entirely
- 9% due to not having the raw payload properly, like your framework will parse the JSON and give it back to you "tainted/changed" which breaks signature verification
- 1% due to something else
Can you explain exactly how you're testing this? Are you using the Stripe CLI or ngrok or something else? Can you show the exact code for your route?
cc @hushed cypress
We're not using manual verification - I pointed to those docs because they had an explanation of the v0 vs v1 signature values that seemed relevant. I did some manual verification just for understanding what the stripe library's code was doing. Here's an abridged version of our Django view. This is handling an actual test-mode webhook event for a webhook endpoint on a public facing test server, configured in our Stripe test-mode dashboard. Not using ngrok or stripe CLI.
def stripe_webhook_handler(request):
try:
signature = request.META["HTTP_STRIPE_SIGNATURE"]
except KeyError as exc:
logging.exception(exc)
# No signature, so we can't verify event.
return HttpResponseBadRequest()
logging.info("signature: %r", signature)
# request.body is the raw incoming byte string, unparsed and unmodified by any middleware. Spaces, tabs, newlines, key ordering, etc, will be exactly what stripe POSTed.
logging.info("request.body: %r", request.body)
# Try both secrets
for secret in (
settings.STRIPE_DIRECT_WEBHOOK_TOKEN,
settings.STRIPE_CONNECT_WEBHOOK_TOKEN,
):
try:
event = stripe.Webhook.construct_event(
payload=request.body,
sig_header=signature,
secret=secret,
)
except (ValueError, stripe.error.SignatureVerificationError) as exc:
# Invalid payload or signature
logging.exception(exc)
continue
else:
process_event(event)
return HttpResponse()
return HttpResponseBadRequest()
payload=request.body,
99% sure that's your issue
your framework is likely trying to be helpful and parsed the raw JSON first and the version you get is not the one Stripe signed (like wht @merry steeple explained earlier)
We don't have a Django specific example I can point you to, but it's almost always the problem and you want to find a way to force django to give you the real raw POST body
I'll double-check our middleware list for anything suspicious, but I don't believe Django does this on its own. https://docs.djangoproject.com/en/4.1/ref/request-response/#django.http.HttpRequest.body says:
HttpRequest.body¶
The raw HTTP request body as a bytestring. This is useful for processing data in different ways than conventional HTML forms: binary images, XML payload etc. For processing conventional form data, use HttpRequest.POST.
yeah I know but I can tell you it's 99% the issue
Thanks!
you also mention 2 webhooks, so the other possibility is using the wrong secret
I'll start there. I'll report back if I find that to be the issue.
Are you familiar with any other language like php or ruby?
For sure
you could do a really simple example to try and make that work first
and once you're sure you have the right raw body + secret in that language you can more easily compare
you can also try our quickstart in Python but it's with Flask: https://stripe.com/docs/webhooks/quickstart
And sorry, I know this is really frustrating. There is no easy "visual feedback" in what's wrong, just the signature works or doesn't 😦