#5what - Webhook signatures

1 messages · Page 1 of 1 (latest)

hexed oar
#

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

#

Would you be open to using our official libraries instead of doing manual signature verification?

hushed cypress
#

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

hexed oar
#

Sorry for the delay! I'm getting some help from a colleague

coarse forum
#

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

merry steeple
#

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.

coarse forum
#

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

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()
coarse forum
#

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

hushed cypress
#

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.

coarse forum
#

yeah I know but I can tell you it's 99% the issue

hushed cypress
#

Thanks!

coarse forum
#

you also mention 2 webhooks, so the other possibility is using the wrong secret

hushed cypress
#

I'll start there. I'll report back if I find that to be the issue.

coarse forum
#

Are you familiar with any other language like php or ruby?

hushed cypress
#

For sure

coarse forum
#

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

#

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 😦