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
});
}
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.