Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add webhook verify utility function to node #457

Merged
merged 14 commits into from
Jul 19, 2022

Conversation

mjcuva
Copy link
Member

@mjcuva mjcuva commented May 16, 2022

Added a utility function to make verifying that a webhook request is legitimately from ReadMe easier!

Currently we recommend users have the following code in their endpoint:

  // Verify signature
  const [fullMatch, time, readmeSignature] = /t\=(.*),v0\=(.*)/.exec(req.headers["readme-signature"]);
  
  const unsigned = `${time}.${JSON.stringify(body)}`;
  const hmac = crypto.createHmac('sha256', 'PROJECT_SECRET');
  const verifySignature = hmac.update(unsigned).digest('hex');
  if (verifySignature !== readmeSignature) {
    return {};
  }

This moves this logic to a readme.verify function so we don't have to explain the security of how this weeks (though we should keep the steps for doing this manually in the docs like stripe does).

With this new function the code will look like this instead:

  // Verifies the request is legitimate and came from ReadMe
  const signature = req.headers['readme-signature'];
  const secret = '12321313'
  try {
    readme.verify(req.body, signature, secret);
  } catch (e) {
   // INVALID
    return res.sendStatus(401);
  }

Even though the number of lines of code is similar, it's a lot less complex. I also added code in readme.verify that checks the timestamp is recent to mitigate against replay attacks, which we should probably also be doing in the manual version.

We should also do this in the rest of the metrics sdks as well!

@mjcuva mjcuva requested a review from domharrington May 16, 2022 23:03
Copy link
Member

@domharrington domharrington left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this in principal for sure! Few comments inline. Can you add some docs for this? Wanna take a stab at adding for other languages, or do you think it's okay to merge in for JS initially?

email: String;
}

export function verify(body: WebhookBody, signature: string, secret: string): WebhookBody {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you feel about calling this verifyWebhook() instead? verify on it's own is a little generic.

packages/node/src/lib/webhook-verify.ts Outdated Show resolved Hide resolved
@erunion erunion added area:node enhancement New feature or request labels Jun 30, 2022
This is in core and doesnt need to come from npm
This test was throwing an error, but not the Expired Signature error
that we were expecting. It was throwing because we were passing in
`new Date()` into the test instead of `Date.now()`.

I've fixed all of the tests here, and ensured that we're casting to int
when constructing the new Date() in verify()
});
}

const randomApiKey = 'rdme_abcdefghijklmnopqrstuvwxyz';

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "rdme_abcdefghijklmnopqrstuvwxyz" is used as [key](1).
Comment on lines 15 to 34
app.post('/webhook', express.json({ type: 'application/json' }), (req, res) => {
// Verify the request is legitimate and came from ReadMe
const signature = req.headers['readme-signature'];
// Your ReadMe secret
const secret = process.env.README_API_KEY;
try {
readme.verify(req.body, signature, secret);
} catch (e) {
// Handle invalid requests
return res.sendStatus(401);
}
// Fetch the user from the db
db.find({ email: req.body.email }).then(user => {
return res.json({
// OAS Security variables
petstore_auth: 'default-key',
basic_auth: { user: 'user', pass: 'pass' },
});
});
});

Check failure

Code scanning / CodeQL

Missing rate limiting

This route handler performs [authorization](1), but is not rate-limited.
Copy link
Member

@domharrington domharrington left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty happy about this apart from two things:

@mjcuva
Copy link
Member Author

mjcuva commented Jul 18, 2022

@domharrington Maybe we should just go with verifyWebhook? I kinda like the explicitness I think?

Other than my one small comment I'm happy with this!

describe('verifyWebhook()', () => {
it('should return the body if the signature is valid', () => {
const body = { email: 'marc@readme.io' };
const secret = 'docs4dayz';

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "docs4dayz" is used as [key](1). The hard-coded value "docs4dayz" is used as [key](2).

it('should throw an error if signature is invalid', () => {
const body = { email: 'marc@readme.io' };
const secret = 'docs4dayz';

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "docs4dayz" is used as [key](1).
const secret = 'docs4dayz';
const time = Date.now();
const unsigned = `${time}.${JSON.stringify(body)}`;
const hmac = crypto.createHmac('sha256', 'invalidsecret');

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "invalidsecret" is used as [key](1).

it('should throw an error if timestamp is too old', () => {
const body = { email: 'marc@readme.io' };
const secret = 'docs4dayz';

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "docs4dayz" is used as [key](1). The hard-coded value "docs4dayz" is used as [key](2).

it('should throw an error if signature is missing', () => {
const body = { email: 'marc@readme.io' };
const secret = 'docs4dayz';

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "docs4dayz" is used as [key](1).
@domharrington domharrington merged commit 3cefdb9 into main Jul 19, 2022
@domharrington domharrington deleted the node/webhook-verify branch July 19, 2022 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants