From 5f9d31c7c914e5cce6edc38c115e88d3ee3532a0 Mon Sep 17 00:00:00 2001 From: Mikk Andresen Date: Tue, 8 Jan 2019 18:04:17 +0200 Subject: [PATCH] WIP: Fixes to the cosJwt and added more tests - https://github.com/citizenos/citizenos-api/issues/70 --- libs/cosJwt.js | 18 ++++--- test/libs/cosJwt.js | 127 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/libs/cosJwt.js b/libs/cosJwt.js index fe11e8184..9605c0ba9 100644 --- a/libs/cosJwt.js +++ b/libs/cosJwt.js @@ -5,7 +5,7 @@ * * @param {Object} app Express app * - * @returns {Object} + * @returns {Function} Function */ module.exports = function (app) { @@ -23,16 +23,20 @@ module.exports = function (app) { /** * Get restricted use token * - * @param {Object} payload Payload object to sign, note that "scope" property is reserved and will throw an error! + * @param {Object} payload Payload object to sign, note that "audience" property is reserved and will throw an error! * @param {Array|String} audience Array of allowed audiences (usage scopes, paths with methods). For ex: ["POST /api/new/stuff", "GET /api/foo/bar"]. Audience is originally part of the jwt.sign options, but bringing it out separately as it is required by all tokens we issue. - * @param {Object} options jwt.sign options like expiresIn etc (https://github.com/auth0/node-jsonwebtoken/tree/cb33aabc432408ed7f3826c2f5b5930313b63f1e) + * @param {Object} [options] jwt.sign options like expiresIn etc (https://github.com/auth0/node-jsonwebtoken/tree/cb33aabc432408ed7f3826c2f5b5930313b63f1e) * * @private * * @returns {Promise} Promise */ var _getTokenRestrictedUse = function (payload, audience, options) { - if (!audience) { + if (!payload) { + throw new Error('Missing required parameter "payload"'); + } + + if (!audience || !audience.length) { throw new Error('Missing required parameter "audience". Please specify scope to which the usage is restricted!'); } @@ -49,8 +53,8 @@ module.exports = function (app) { } }); - - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { + // Interesting, jwt.sign (5.7.0) has no err object in the callback, the first argument is always the result. Code That Never Fails (tm) jwt.sign(payload, config.session.privateKey, effectiveOptions, function (token) { return resolve(token); }); @@ -62,7 +66,7 @@ module.exports = function (app) { * * @param {string} token JWT token * @param {string} audience Audience that is required. The format is "METHOD PATH". For example "POST /api/new/stuff". Audience is originally part of the jwt.verify options, but bringing it out separately as it is required by all tokens we issue. - * @param {Object} options jwt.verify options. + * @param {Object} [options] jwt.verify options. * * @private * diff --git a/test/libs/cosJwt.js b/test/libs/cosJwt.js index 1667b4615..fdf358558 100644 --- a/test/libs/cosJwt.js +++ b/test/libs/cosJwt.js @@ -6,6 +6,7 @@ suite('cosJwt', function () { var app = require('../../app'); var cosJwt = app.get('cosJwt'); + var jwt = app.get('jwt'); suiteTeardown(function (done) { shared @@ -24,10 +25,136 @@ suite('cosJwt', function () { }) .then(function (decoded) { assert.equal(decoded.foo, testPayload.foo); + assert.deepEqual(decoded.aud, [testAudience]); return done(); }) .catch(done); }); + test('Success - multiple audiences (scopes)', function (done) { + var testPayload = {foo: 'bar'}; + var testAudiences = ['GET /asd', 'POST /api/foo/bar']; + + cosJwt + .getTokenRestrictedUse(testPayload, testAudiences) + .then(function (token) { + return cosJwt.verifyTokenRestrictedUse(token, testAudiences[0]) + .then(function (decoded) { + assert.equal(decoded.foo, testPayload.foo); + assert.deepEqual(decoded.aud, testAudiences); + + return token; + }); + }) + .then(function (token) { + return cosJwt.verifyTokenRestrictedUse(token, testAudiences[1]); + }) + .then(function (decoded) { + assert.equal(decoded.foo, testPayload.foo); + assert.deepEqual(decoded.aud, testAudiences); + + return done(); + }) + .catch(done); + }); + + + test('Success - expiry', function (done) { + var testPayload = {foo: 'bar'}; + var testAudience = 'POST /api/foo/bar'; + var testOptions = { + expiresIn: '1m' + }; + + cosJwt + .getTokenRestrictedUse(testPayload, testAudience, testOptions) + .then(function (token) { + return cosJwt.verifyTokenRestrictedUse(token, testAudience); + }) + .then(function (decoded) { + assert.equal(decoded.foo, testPayload.foo); + assert.property(decoded, 'exp'); + + return done(); + }) + .catch(done); + }); + + test('Fail - expired token', function (done) { + var testPayload = {foo: 'bar'}; + var testAudience = 'POST /api/foo/bar'; + var testOptions = { + expiresIn: '1ms' + }; + + cosJwt + .getTokenRestrictedUse(testPayload, testAudience, testOptions) + .then(function (token) { + return cosJwt.verifyTokenRestrictedUse(token, testAudience); + }) + .then(function () { + return done(new Error('Should fail due to token expiry!')); + }) + .catch(function (err) { + assert.instanceOf(err, jwt.TokenExpiredError); + + done(); + }); + }); + + test('Fail - invalid audience (scope)', function (done) { + var testPayload = {foo: 'bar'}; + var testAudience = 'POST /api/foo/bar'; + var testInvalidAudience = 'POST /api/invalid'; + var testOptions = { + expiresIn: '1m' + }; + + cosJwt + .getTokenRestrictedUse(testPayload, testAudience, testOptions) + .then(function (token) { + return cosJwt.verifyTokenRestrictedUse(token, testInvalidAudience); + }) + .then(function () { + return done(new Error('Should fail due to invalid audience!')); + }) + .catch(function (err) { + assert.instanceOf(err, jwt.JsonWebTokenError); + assert.equal(err.message, 'jwt audience invalid. expected: ' + testInvalidAudience); + + done(); + }); + }); + + suite('getTokenRestrictedUse', function () { + + test('Fail - missing required parameters - payload', function (done) { + try { + cosJwt.getTokenRestrictedUse(); + } catch (err) { + return done(); + } + done(new Error('Should throw an error if payload parameter is missing!')); + }); + + test('Fail - missing required parameters - audience', function (done) { + try { + cosJwt.getTokenRestrictedUse({foo: 'bar'}); + } catch (err) { + return done(); + } + done(new Error('Should throw an error if audience parameter is missing!')); + }); + + test('Fail - invalid parameters - audience in wrong format', function (done) { + try { + cosJwt.getTokenRestrictedUse({foo: 'bar'}, ['/no/method/for/path']); + } catch (err) { + return done(); + } + done(new Error('Should throw an error if audience parameter value is in invalid format!')); + }); + + }); });