Webhooks HMAC verification


#1

Greetings,

I’m working on trying to verify the signature sent via Patreon with a webhook. I wrote a verification method similar to the code found here, but with Crypto-JS and had no luck. After some research, I found this information here, but was unable to get a verification correctly. Following instructions in that page, I cloned the glitch project and tested with that code and it fails every time. I’ve experimented with it, trying bodyParser.raw(), buffers, and tried ‘utf8’ encoding on update() which all failed.

Any ideas?

Glitch Link: https://glitch.com/edit/#!/alluring-origami
Webhook screenshot: https://i.imgur.com/V7RXzTM.png


#2

The fact that you’re now experiencing this issue too has prompted me to do a little more digging. I have used 3 different Patreon accounts to create a dozen webhooks using the example code. Each webhook created for the first and second account has signatures that verify, however with the third account I have been able to create a webhook that fails signature validation: in fact, all 3 webhooks I have created with that account fail signature validation. The difference between the accounts is registration date: the third account was created later than the first two. The third account does have one webhook that (as far as I know) works – however, it was created 2 months ago. However, I have just registered a brand new account and signatures for those webhooks verify correctly – so I’m not sure if registration date is a red herring. When was your account created?

The issue appears to be that signatures generated for the webhooks of some accounts are not valid. You could try registering a new account to determine if it is indeed an issue specific to your account.

As the Patreon team didn’t seem to reply to the previous thread about this, I’d recommend reaching out to the Patreon team directly (via platform@patreon.com) to ensure that it doesn’t fall through the cracks.

I’m pretty confident there’s a bug with the Patreon system rather than the example code.


#3

Thank you for testing and replying. :slight_smile:

My account is about a month old now, but I did create a test account a few days ago for testing the webhooks. I used that account and tested the webhook with your code and it works.

I will send them an email and make sure to mention both this post and the other post.

Thank you again for your time and help.


#4

Mea culpa. I’m an idiot. I couldn’t think of how the Patreon system could have this issue: how could it generate the wrong signature only sometimes? That doesn’t make sense. So I went over my code again, and again… and found it. The mistake is with my code. I have no idea how I missed this.

The code turns the request body into JSON and then turns it back into a string. You would not expect this to produce different results, after all: valid JSON is valid JSON… but what about encoding? That’s why it fails for some accounts, and not others, it’s because of values that are sometimes appearing in free-input fields (e.g: campaign summary).

The reason there’s differences between the strings appears to be that the Patreon parser inserts a NO-BREAK SPACE character (\u00a0) before links, e.g:

\u00a0<a href=\"https://example.com\">example.com</a>

However, during the process of turning the request body into JSON and then back into a string, these characters are lost and it becomes:

<a href=\"https://example.com\">example.com</a>

And, of course:

'<a href=\"https://example.com\">example.com</a>' !== '\u00a0<a href=\"https://example.com\">example.com</a>';

So, there we have it. I’ll go back and update my posts in the other thread so that anybody coming across the example code via Google doesn’t have these issues.

The fix is to treat the request body as a string the moment it arrives, e.g:

const express = require('express')
const app = express()
const crypto = require('crypto')
const bodyParser = require('body-parser')

app.use(bodyParser.text({type: '*/*'}));

app.post("/webhook", (request, response) => {
  const webhookSecret = 'secret';
  
  let hash = crypto.createHmac('md5', webhookSecret).update(request.body).digest('hex');
  let success = (request.header('x-patreon-signature') === hash);
  
  console.log('Signature received: ' + request.header('x-patreon-signature'));
  console.log('Signature generated: ' + hash);
  console.log('Signature validation status: ' + success);
    
  return response.status(success ? 200 : 400).json({result: success});
})

app.listen(process.env.PORT)

#5

Yup! That was it. Good catch!
I appreciate all of your help.


#6

Thanks for the tip, if you use a request body parser have a look if it has an option of giving you the raw body string straight. Eg. in case of https://github.com/koajs/bodyparser, ctx.request.rawBody is available…