Skip to content

Instantly share code, notes, and snippets.

@coolaj86
Last active October 23, 2018 20:45
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save coolaj86/81a3b61353d2f0a2552c to your computer and use it in GitHub Desktop.
Save coolaj86/81a3b61353d2f0a2552c to your computer and use it in GitHub Desktop.

Here's how you validate a mailgun webhook in Node.js (as per the mailgun docs for securing webhooks)

'use strict';

var scmp = require('scmp')
  , crypto = require('crypto')
  . mailgunPrivateKey = 'XXXXXXXXXXXXX'
  , mailgunTokens = {}
  , mailgunExpirey = 15 * 60 * 1000
  , mailgunHashType = 'sha256'
  , mailgunSignatureEncoding = 'hex'
  ;

function validateMailgun(apiKey, timestamp, token, signature) {
  var actual
    , adjustedTimestamp = parseInt(timestamp, 10) * 1000
    , fresh = (Math.abs(Date.now() - adjustedTimestamp) < mailgunExpirey)
    ;

  if (!fresh) {
    console.error('[mailgun] Stale Timestamp: this may be an attack');
    console.error('[mailgun] However, this is most likely your fault\n');
    console.error('[mailgun] run `ntpdate ntp.ubuntu.com` and check your system clock\n');
    console.error('[mailgun] System Time: ' + new Date().toString());
    console.error('[mailgun] Mailgun Time: ' + new Date(adjustedTimestamp).toString(), timestamp);
    console.error('[mailgun] Delta: ' + (Date.now() - adjustedTimestamp));
    return false;
  }

  if (mailgunTokens[token]) {
    console.error('[mailgun] Replay Attack');
    return false;
  }
  
  mailgunTokens[token] = true;

  setTimeout(function () {
    delete mailgunTokens[token];
  }, mailgunExpirey + (5 * 1000));

  return scmp(
    signature
  , crypto.createHmac(mailgunHashType, apiKey)
    .update(new Buffer(timestamp + token, 'utf-8'))
    .digest(mailgunSignatureEncoding)
  );
}

function router(app) {
  app.post('/webhooks/mailgun/*', function (req, res, next) {
    var body = req.body
      ;

    if (!validateMailgun(mailgunPrivateKey, body.timestamp, body.token, body.signature)) {
      console.error('Request came, but not from Mailgun');
      res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } });
      return;
    }

    next();
  });

  app.post('/webhooks/mailgun/catchall', function (req, res) {
    // actually handle request here
  });
}
@coolaj86
Copy link
Author

coolaj86 commented Jul 5, 2017

Note: needs multipart/form-data parser (not urlencoded) for mailgun relpy

@coolaj86
Copy link
Author

coolaj86 commented Jul 6, 2017

Note: the "store and notify" uses urlencoded forms and some of the fields will be too large for many "secure" parsers which expect boundaries and large fields with many strange escape sequences.

@rvanmil
Copy link

rvanmil commented Jan 11, 2018

Thanks for sharing! 👍

@wakatanka
Copy link

Hi, thanks for sharing, i'm trying your script and got this error:
Error: Both scmp args must be Buffers

can you help me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment