X-Hubspot-Signature python validation for Hubspot App

SOLVE
skirkpatrick
Member

I haven't been able to work out how to recompute the X-Hubspot-Signature successfully at my server when a data fetch request is made for a CRM card set up on the app. I have confirmed that my code using the python hashlib as described in the documentation works with the provided examples:

 

https://developers.hubspot.com/docs/faq/v2-request-validation

 

When I use my app's client secret and build the string as described to produce the hexdigest, my computed value does not match the value provided in the X-Hubspot-Signature header. I checked to confirm that the protocol wasn't the issue as indicated in other posts (not http vs https).

 

The data fetch includes query params and a property that I configured for the card's target record type (Deals). I have tried both removing the query params and including them when computing, but still no luck.

 

Anyone have any advice they can offer?

0 Upvotes
1 Accepted solution

Accepted Solutions
skirkpatrick
Solution
Member

Many thanks to @WendyGoh for helping me work towards a solution!

 

I may have missed this in the documentation, but Hubspot computes the signature BEFORE url-encoding the param values.

 

The reason that my signature computation was not correct was my handling of url-encoded parameters in the full request url hitting my server. There were a couple of specific details, though, that I was able to suss out.

 

One param hitting my server contained an email address. When taking the raw request to compute the signature, the '@' was still encoded as '%40'. The email address value had to be url-decoded prior to computing the signature.

 

The other param that was causing trouble was a bit thornier. We have a custom property that contains a url as the value, with query params on that url. For demonstration purposes, the param value is received in the raw request url param as something like:

 

my_custom_prop=https:%2F%2Fmy.server.com%2Findex%2Fuid%2F123456789%3F___param%3Dvalue

Note that the entire param value is url-encoded. After url-decoding the value becomes:

https://my.server.com/index/uid/123456789?___param=value

Except this also fails the signature computation. The query params on this url property value need to remain url-encoded for the Hubspot signature computation to match. In the end this needed to be:

https://my.server.com/index/uid/123456789?___param%3Dvalue

Once I updated my code to ensure the query params of the Hubspot request were properly url-encoded and url-decoded as described above, the signature computation worked as expected.

 

This is a generally handy tool that I hadn't come across before that @WendyGoh turned me on to: https://requestinspector.com/

 

Hope this helps someone in the future!

View solution in original post

9 Replies 9
WendyGoh
HubSpot Employee

Hi @skirkpatrick,

 

Could you share with me the python script that you wrote to verify the v2 request signature? 

 

Are you concatenating the client secret + HTTP method + URI + Request body?

0 Upvotes
skirkpatrick
Member

Hi @WendyGoh - Sure! Here's the snippet that's pulling the appropriate request fields to construct the signature:

 

    source = request.method + request.url
    if request.data:
        source += request.data
    print('{validateHubspotSignature} source: <app_secret>%s' % (source))
    source = os.environ.get('HUBSPOT_APP_CLIENT_SECRET', 'HUBSPOT_APP_CLIENT_SECRET') + source
    challenge = hashlib.sha256(source.encode('utf-8')).hexdigest()
    headerSig = request.headers.get('X-Hubspot-Signature')
    print('{validateHubspotSignature} %s (computed) vs %s (header)' % (challenge, headerSig))

I'm printing out the Hubspot header vs my computed value to test my results. I've tried several variations on the url used, but haven't had any luck identifying the problem.

0 Upvotes
WendyGoh
HubSpot Employee

Hey @skirkpatrick,

 

Thanks for providing the snippet!

 

When comparing the python code here - https://developers.hubspot.com/docs/faq/v2-request-validation, specifically this line:

hashlib.sha256(source_string).hexdigest()

It didn't include 

encode('utf-8')

and so could you try modifying the code to be:

challenge = hashlib.sha256(source).hexdigest()

instead of 

challenge = hashlib.sha256(source.encode('utf-8')).hexdigest()

and see if it works?

0 Upvotes
skirkpatrick
Member

Apologies, I should have mentioned that I get the following error without the utf-8 encoding:

 

    challenge = hashlib.sha256(source).hexdigest()
TypeError: Unicode-objects must be encoded before hashing

The python server is running version 3.7.4 of python.

0 Upvotes
skirkpatrick
Member

Good morning, @WendyGoh ! Just thought I would bump this thread to see if you had any additional guidance. Thanks!

0 Upvotes
WendyGoh
HubSpot Employee

Hi @skirkpatrick,

 

Apologise for the delayed in response.

 

At a glance the code looks perfectly fine. In this case, let's try using the exact python code that HubSpot documentation here: Validating the v2 request signature, and see if it works?

 

>>> import hashlib

>>> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
>>> http_method = 'GET'
>>> http_uri = 'https://www.example.com/webhook_uri'
>>> source_string = client_secret + http_method + http_uri
>>> source_string
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyGEThttps://www.example.com/webhook_uri'
>>> hashlib.sha256(source_string).hexdigest()
'eee2dddcc73c94d699f5e395f4b9d454a069a6855fbfa152e91e88823087200e'
0 Upvotes
skirkpatrick
Member

Hi @WendyGoh - no worries!

 

I have run that code successfully, but I did still have to utf-8 encode the `source_string`. Here is my exact console output in my runtime env:

 

Python 3.7.4 (default, Sep 12 2019, 15:51:10) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hashlib
>>> client_secret = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
>>> http_method = 'GET'
>>> http_uri = 'https://www.example.com/webhook_uri'
>>> source_string = client_secret + http_method + http_uri
>>> source_string
'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyGEThttps://www.example.com/webhook_uri'
>>> hashlib.sha256(source_string).hexdigest()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Unicode-objects must be encoded before hashing
>>> hashlib.sha256(source_string.encode('utf-8')).hexdigest()
'eee2dddcc73c94d699f5e395f4b9d454a069a6855fbfa152e91e88823087200e'

When I attempt to compute the signature on my server using my app secret and the documented fields, my hexdigest still does not match. Would it be helpful to take this offline so I can send along my app specifics?

 

Best,

-Sean

 

0 Upvotes
WendyGoh
HubSpot Employee

Hey @skirkpatrick,

 

Sounds good!

 

Can you direct message me the following:

1. Your app id

2. The url of an object in HubSpot (contact, company or deal record) where you've seen this happen

 

Would like to do some testing on my end. Thank you!

0 Upvotes
skirkpatrick
Solution
Member

Many thanks to @WendyGoh for helping me work towards a solution!

 

I may have missed this in the documentation, but Hubspot computes the signature BEFORE url-encoding the param values.

 

The reason that my signature computation was not correct was my handling of url-encoded parameters in the full request url hitting my server. There were a couple of specific details, though, that I was able to suss out.

 

One param hitting my server contained an email address. When taking the raw request to compute the signature, the '@' was still encoded as '%40'. The email address value had to be url-decoded prior to computing the signature.

 

The other param that was causing trouble was a bit thornier. We have a custom property that contains a url as the value, with query params on that url. For demonstration purposes, the param value is received in the raw request url param as something like:

 

my_custom_prop=https:%2F%2Fmy.server.com%2Findex%2Fuid%2F123456789%3F___param%3Dvalue

Note that the entire param value is url-encoded. After url-decoding the value becomes:

https://my.server.com/index/uid/123456789?___param=value

Except this also fails the signature computation. The query params on this url property value need to remain url-encoded for the Hubspot signature computation to match. In the end this needed to be:

https://my.server.com/index/uid/123456789?___param%3Dvalue

Once I updated my code to ensure the query params of the Hubspot request were properly url-encoded and url-decoded as described above, the signature computation worked as expected.

 

This is a generally handy tool that I hadn't come across before that @WendyGoh turned me on to: https://requestinspector.com/

 

Hope this helps someone in the future!

View solution in original post