UNPKG

hsudb

Version:

HMAC (stored in the database) URL signing middleware for Express

1,118 lines (800 loc) 37.7 kB
var url = require('url'), querystring = require('querystring'), express = require('express'), request = require('supertest'), expect = require('chai').expect, rndm = require('rndm'), hsudb = require('../'); /** * Helper function to create an instance of an Express app. * @return {Object} The Express app. */ function createApp () { // create the express app var app = express(); return app; } /** * Helper function to create a supertest agent for testing HSUDB against. * @return {TestAgent} A supertest agent configured against the Express app. */ function createAgent (app) { return request.agent(app); } describe('HSUDB', function () { var tamperedErrorHandler = function (err, req, res, next) { if (err.code !== 'EBADHMACDIGEST') { return next(err); } res.status(403).end('URL has been tampered with'); }, timedOutErrorHandler = function (err, req, res, next) { if (err.code !== 'ETIMEOUTHMACDIGEST') { return next(err); } res.status(403).end('URL has timed out'); }; it('must be passed a secret', function () { var fn = function () { var middleware = hsudb(); // eslint-disable-line no-unused-vars }; expect(fn).to.throw(Error, /secret/); }); it('must be passed a store function', function () { var fn = function () { var middleware = hsudb({ // eslint-disable-line no-unused-vars secret: 'secret' }); }; expect(fn).to.throw(Error, /store/); }); it('must be passed a retrieve function', function () { var fn = function () { var middleware = hsudb({ // eslint-disable-line no-unused-vars secret: 'secret', store: function () { } }); }; expect(fn).to.throw(Error, /retrieve/); }); it('must be passed a complete function', function () { var fn = function () { var middleware = hsudb({ // eslint-disable-line no-unused-vars secret: 'secret', store: function () { }, retrieve: function () { } }); }; expect(fn).to.throw(Error, /complete/); }); describe('returns a scoping function that', function () { it('must be passed an id', function () { var fn = function () { var hsudbProtect = hsudb({ // eslint-disable-line no-unused-vars secret: 'secret', store: function () { }, retrieve: function () { }, complete: function () { } })(); }; expect(fn).to.throw(Error, /id/); }); it('returns three stages of middleware', function () { var id = rndm(), hsudbProtect = hsudb({ // eslint-disable-line no-unused-vars secret: 'secret', store: function () { }, retrieve: function () { }, complete: function () { } }); expect(hsudbProtect(id)).to.have.property('setup'); expect(hsudbProtect(id)).to.have.property('verify'); expect(hsudbProtect(id)).to.have.property('complete'); }); it('allows multiple instances of HSUDB to run concurrently', function (done) { var idOne = rndm(), idTwo = rndm(), urlToSignOne = '/one?user=6dg3tct749fj&ion=1&espv=2', urlToSignTwo = '/two?user=6dg3tct749fj&ion=1&espv=2', hsudbProtectOne = hsudb({ secret: 'secret', store: function (req, id, salt, callback) { expect(id).to.equal(idOne); expect(id).to.not.equal(idTwo); }, retrieve: function () { }, complete: function () { } }), hsudbProtectTwo = hsudb({ secret: 'secret', store: function (req, id, salt, callback) { expect(id).to.equal(idTwo); expect(id).to.not.equal(idOne); }, retrieve: function () { }, complete: function () { } }); var app = createApp(); app.get('/pre/one', hsudbProtectOne(idOne).setup, function (req, res, next) { req.signUrl(urlToSignOne); res.status(200).end(); }); app.get('/pre/two', hsudbProtectTwo(idTwo).setup, function (req, res, next) { req.signUrl(urlToSignTwo); res.status(200).end(); }); var agent = createAgent(app); agent .get('/pre/one') .expect(200, function (errOne, res) { if (errOne) { return done(errOne); } agent .get('/pre/two') .expect(200, done); }); }); describe('returns three stages of middleware,', function () { describe('.setup', function () { describe('provides a signUrl function', function () { it('that is available on a route which has the .setup middleware', function (done) { var id = rndm(), app = createApp(), hsudbProtect = hsudb({ secret: 'secret', store: function () { }, retrieve: function () { }, complete: function () { } }); app.get('/', hsudbProtect(id).setup, function (req, res, next) { return res.status(200).send(Object.keys(req).indexOf('signUrl') >= 0 && typeof req.signUrl === 'function'); }); createAgent(app) .get('/') .expect(200, 'true', done); }); it('will request to store a salt', function (done) { var id = rndm(), app = createApp(), store, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { return done(); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: function () { }, complete: function () { } }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { var urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2'; req.signUrl(urlToSign); }); createAgent(app) .get('/') .end(); }); describe('which returns a promise', function () { it('that will resolve if everything works', function (done) { var id = rndm(), app = createApp(), store, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { return callback(); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: function () { }, complete: function () { } }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { var urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2'; req.signUrl(urlToSign).then(function (signedUrl) { expect(signedUrl).to.contain(urlToSign); return res.status(200).end(); }, done); }); createAgent(app) .get('/') .expect(200, done); }); it('that will reject if an error is returned to the store function callback', function (done) { var id = rndm(), app = createApp(), store, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { return callback(new Error('Failed to store the salt.')); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: function () { }, complete: function () { } }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { var urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2'; req.signUrl(urlToSign).then(function (signedUrl) { expect(signedUrl).to.contain(urlToSign); return res.status(200).end(); }, function (err) { return res.status(500).send(err.message); }); }); createAgent(app) .get('/') .expect(500, /Failed to store the salt/, done); }); }); describe('accepts callbacks', function () { it('that will be executed without an error if everything works', function (done) { var id = rndm(), app = createApp(), store, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { return callback(); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: function () { }, complete: function () { } }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { var urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2'; req.signUrl(urlToSign, done); }); createAgent(app) .get('/') .end(); }); it('that will be passed an error if an error is returned from the store function', function (done) { var id = rndm(), app = createApp(), store, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { return callback(new Error('Failed to store the salt.')); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: function () { }, complete: function () { } }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { var urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2'; req.signUrl(urlToSign, function (err) { expect(err).to.exist; expect(err.message).to.equal('Failed to store the salt.'); return done(); }); }); createAgent(app) .get('/') .end(); }); }); }); }); describe('.verify', function () { it('will timeout after the specified TTL', function (done) { this.timeout(3000); var id = rndm(), app = createApp(), agent = createAgent(app), db = {}, urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2', signedUrl, store, retrieve, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { db[_id] = salt; return callback(); }; // define the retrieve function retrieve = function (req, _id, callback) { return callback(null, db[_id]); }; // create an instance of hsudb hsudbProtect = hsudb({ ttl: 1, secret: 'secret', store: store, retrieve: retrieve, complete: function () { } }) app.get('/sign-url', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).end(); }, done); }); app.get('/entry', hsudbProtect(id).verify, function (req, res, next) { res.status(200).send('entered'); }); app.use(timedOutErrorHandler); agent .get('/sign-url') .expect(200, function (err, res) { if (err) { return done(err); } setTimeout(function () { agent .get(url.parse(signedUrl, true).path) .expect(403, /timed out/, done); }, 2000); }); }); it('will use a custom urlToVerify method if provided', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), urlToSign = '/verify?user=6dg3tct749fj&ion=1&espv=2', db = {}, signedUrl, store, retrieve, hsudbProtect, urlToVerify, called = false; // define the store function store = function (req, _id, salt, callback) { db[_id] = salt; return callback(); }; retrieve = function (req, _id, callback) { return callback(null, db[_id]); }; // A custom method to determine the URL to verify. urlToVerify = function (req, _id) { called = true; return req.body.signedUrl; }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: function () {}, urlToVerify: urlToVerify }) app.use(require('body-parser').json()); app.get('/', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).end(); }, done); }); app.post('/verify-proxy', hsudbProtect(id).verify, function (req, res, next) { res.status(200).send('entered'); expect(called).to.be.true; return done(); }); agent .get('/') .expect(200, function (err, res) { if (err) { return done(err); } agent .post('/verify-proxy') .send({ 'signedUrl': signedUrl }) .end(); }); }); describe('executes the retrieve hook', function () { it('to retrieve a salt', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), urlToSign = '/verify?user=6dg3tct749fj&ion=1&espv=2', signedUrl, store, retrieve, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { return callback(); }; retrieve = function (req, _id, callback) { return done() }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: function () { } }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).end(); }, done); }); app.get('/verify', hsudbProtect(id).verify, function (req, res, next) { res.status(200).send('entered'); }); agent .get('/') .expect(200, function (err, res) { if (err) { return done(err); } agent .get(url.parse(signedUrl, true).path) .end(); }); }); it('and will accept an error and pass it on', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), urlToSign = '/verify?user=6dg3tct749fj&ion=1&espv=2', signedUrl, store, retrieve, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { return callback(); }; retrieve = function (req, _id, callback) { return callback(new Error('Failed to retrieve the salt.')); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: function () { } }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).end(); }, done); }); app.get('/verify', hsudbProtect(id).verify, function (req, res, next) { res.status(200).send('entered'); }); app.use(function (err, req, res, next) { expect(err).to.exist expect(err.message).to.equal('Failed to retrieve the salt.'); return done(); }); agent .get('/') .expect(200, function (err, res) { if (err) { return done(err); } agent .get(url.parse(signedUrl, true).path) .end(); }); }); }); }); describe('.complete', function () { describe('executes the complete', function () { it('and will request to remove a salt', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), urlToSign = '/verify?user=6dg3tct749fj&ion=1&espv=2', db = {}, signedUrl, store, retrieve, complete, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { db[_id] = salt; return callback(); }; retrieve = function (req, _id, callback) { return callback(null, db[_id]); }; complete = function (req, _id, callback) { return done(); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: complete }) app.get('/', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).end(); }, done); }); app.get('/verify', hsudbProtect(id).verify, function (req, res, next) { res.status(200).send('entered'); }); app.get('/complete', hsudbProtect(id).complete, function (req, res, next) { req.hsudbComplete(); res.status('200').send('complete'); }); agent .get('/') .expect(200, function (err, res) { if (err) { return done(err); } agent .get(url.parse(signedUrl, true).path) .expect(200, 'entered', function (signedErr, signedRes) { if (signedErr) { return done(signedErr); } agent .get('/complete') .end(); }); }); }); }); }); describe('that collectively will sign a URL and', function () { it('verify the url', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), db = {}, urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2', signedUrl, store, retrieve, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { db[_id] = salt; return callback(); }; // define the retrieve function retrieve = function (req, _id, callback) { return callback(null, db[_id]); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: function () { } }) app.get('/sign-url', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).end(); }, done); }); app.get('/entry', hsudbProtect(id).verify, function (req, res, next) { res.status(200).send('entered'); }); agent .get('/sign-url') .expect(200, function (err, res) { if (err) { return done(err); } agent .get(url.parse(signedUrl, true).path) .expect(200, 'entered', done); }); }); it('403 upon verification failure', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), db = {}, urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2', signedUrl, store, retrieve, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { db[_id] = salt; return callback(); }; // define the retrieve function retrieve = function (req, _id, callback) { return callback(null, db[_id]); }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: function () { } }) app.get('/sign-url', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { // tamper with the url var tamperedUrl = url.parse(_signedUrl, true); tamperedUrl.query.user += 1; tamperedUrl.search = querystring.stringify(tamperedUrl.query); signedUrl = tamperedUrl.format(); return res.status(200).end(); }, done); }); app.get('/entry', hsudbProtect(id).verify, function (req, res, next) { res.status(200).send('entered'); }); app.use(tamperedErrorHandler); agent .get('/sign-url') .expect(200, function (err, res) { if (err) { return done(err); } agent .get(url.parse(signedUrl, true).path) .expect(403, /tampered/, done); }); }); it('will allow the process to repeat', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), db = {}, urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2', signedUrl, store, retrieve, complete, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { db[_id] = salt; return callback(); }; // define the retrieve function retrieve = function (req, _id, callback) { return callback(null, db[_id]); }; // define the complete function complete = function (req, _id) { delete db[_id]; }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: complete }) app.get('/sign-url', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).send('signed'); }, done); }); app.get('/entry', hsudbProtect(id).verify, hsudbProtect(id).complete, function (req, res, next) { // complete the process req.hsudbComplete(); // make sure the database is empty expect(Object.keys(db)).to.be.empty; // we're done res.status(200).send('complete'); }); app.use(tamperedErrorHandler); agent .get('/sign-url') .expect(200, 'signed', function (err, res) { if (err) { return done(err); } agent .get(url.parse(signedUrl, true).path) .expect(200, 'complete', function (completeErr, completeRes) { if (completeErr) { return done(completeErr); } // start the process again agent .get('/sign-url') .expect(200, function (repeatErr, repeatRes) { if (repeatErr) { return done(repeatErr); } agent .get(url.parse(signedUrl, true).path) .expect(200, 'complete', done) }); }); }); }); it('will 403 if request repeated after completion', function (done) { var id = rndm(), app = createApp(), agent = createAgent(app), db = {}, urlToSign = '/entry?user=6dg3tct749fj&ion=1&espv=2', signedUrl, store, retrieve, complete, hsudbProtect; // define the store function store = function (req, _id, salt, callback) { db[_id] = salt; return callback(); }; // define the retrieve function retrieve = function (req, _id, callback) { return callback(null, db[_id]); }; // define the complete function complete = function (req, _id) { delete db[_id]; }; // create an instance of hsudb hsudbProtect = hsudb({ secret: 'secret', store: store, retrieve: retrieve, complete: complete }) app.get('/sign-url', hsudbProtect(id).setup, function (req, res, next) { req.signUrl(urlToSign).then(function (_signedUrl) { signedUrl = _signedUrl; return res.status(200).send('signed'); }, done); }); app.get('/entry', hsudbProtect(id).verify, hsudbProtect(id).complete, function (req, res, next) { // complete the process req.hsudbComplete(); // make sure the database is empty expect(Object.keys(db)).to.be.empty; // we're done res.status(200).send('complete'); }); app.use(tamperedErrorHandler); agent .get('/sign-url') .expect(200, 'signed', function (err, res) { if (err) { return done(err); } agent .get(url.parse(signedUrl, true).path) .expect(200, 'complete', function (completeErr, completeRes) { if (completeErr) { return done(completeErr); } // repeat the request agent .get(url.parse(signedUrl, true).path) .expect(403, done); }); }); }); }); }); }); });