Skip to content

Commit

Permalink
WIP: First version of cosJwt with a happy case test - #70
Browse files Browse the repository at this point in the history
  • Loading branch information
tiblu committed Jan 8, 2019
1 parent 78d4def commit ea5f182
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 12 deletions.
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ app.set('util', require('./libs/util'));
app.set('ddsClient', require('./libs/ddsClient'));
app.set('cosBdoc', require('./libs/cosBdoc')(app));
app.set('cosEtherpad', require('./libs/cosEtherpad')(app));
app.set('cosJwt', require('./libs/cosJwt')(app));

//Config smartId
var smartId = require('./libs/cosSmartId')(app);
Expand Down
91 changes: 91 additions & 0 deletions libs/cosJwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';

/**
* Citizen OS specific JWT logic
*
* @param {Object} app Express app
*
* @returns {Object}
*/
module.exports = function (app) {

var config = app.get('config');
var jwt = app.get('jwt');

var TOKEN_OPTIONS_SIGN_DEFAULTS = {
algorithm: config.session.algorithm
};

var TOKEN_OPTIONS_VERIFY_DEFAULTS = {
algorithms: [config.session.algorithm]
};

/**
* Get restricted use token
*
* @param {Object} payload Payload object to sign, note that "scope" 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)
*
* @private
*
* @returns {Promise} Promise
*/
var _getTokenRestrictedUse = function (payload, audience, options) {
if (!audience) {
throw new Error('Missing required parameter "audience". Please specify scope to which the usage is restricted!');
}

if (options && options.audience) {
throw new Error('Property "audience" is reserved for specifying usage scope of the token. Use "audience" parameter to specify the audience (scope).');
}

var effectiveOptions = Object.assign({}, TOKEN_OPTIONS_SIGN_DEFAULTS, options);
effectiveOptions.audience = typeof audience === 'string' ? [audience] : audience;

effectiveOptions.audience.forEach(function (aud) {
if (!(/^(GET|POST|PUT|DELETE|PATCH) \/.*/).test(aud)) {
throw new Error('Invalid value in "audience" parameter detected - "' + aud + '"');
}
});


return new Promise(function (resolve, reject) {
jwt.sign(payload, config.session.privateKey, effectiveOptions, function (token) {
return resolve(token);
});
});
};

/**
* Verify a restricted use token
*
* @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.
*
* @private
*
* @returns {Promise} Promise
*/
var _verifyTokenRestrictedUse = function (token, audience, options) {
var effectiveOptions = Object.assign({}, TOKEN_OPTIONS_VERIFY_DEFAULTS, options);
effectiveOptions.audience = audience;

return new Promise(function (resolve, reject) {
jwt.verify(token, config.session.publicKey, effectiveOptions, function (err, payload) {
if (err) {
return reject(err);
}

return resolve(payload);
});
});
};


return {
getTokenRestrictedUse: _getTokenRestrictedUse,
verifyTokenRestrictedUse: _verifyTokenRestrictedUse
};
};
33 changes: 33 additions & 0 deletions test/libs/cosJwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

suite('cosJwt', function () {
var assert = require('chai').assert;
var shared = require('../utils/shared');

var app = require('../../app');
var cosJwt = app.get('cosJwt');

suiteTeardown(function (done) {
shared
.closeDb()
.finally(done);
});

test('Success', function (done) {
var testPayload = {foo: 'bar'};
var testAudience = 'POST /api/foo/bar';

cosJwt
.getTokenRestrictedUse(testPayload, testAudience)
.then(function (token) {
return cosJwt.verifyTokenRestrictedUse(token, testAudience);
})
.then(function (decoded) {
assert.equal(decoded.foo, testPayload.foo);

return done();
})
.catch(done);
});

});
24 changes: 12 additions & 12 deletions test/utils/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ var closeDb = function () {
// BUT, as in the universe of these tests there is only one instance of Sequelize, thus if I call sequelize.close() in the suiteTearDown(), next tests will fail cause they cannot get a connection.
// Sequelize has pool.min, pool.idle and pool.evict which clean up the pool, but that by design does not exit Sequelize.
// So I have 2 options: this hack or in suiteSetup each test sets a new Sequelize connection and kills it later. Right now it's the hack.
//if (!interval) {
// interval = setInterval(function () {
// if (db.connectionManager.pool._allObjects.size === 0) {
// clearInterval(interval);
// db
// .close()
// .then(function () {
// logger.info('DB connection force closed from shared.closeDb as the pool was empty.');
// });
// }
// }, 5000);
//}
if (!interval) {
interval = setInterval(function () {
if (db.connectionManager.pool._allObjects.size === 0) {
clearInterval(interval);
db
.close()
.then(function () {
logger.info('DB connection force closed from shared.closeDb as the pool was empty.');
});
}
}, 5000);
}

return Promise.resolve();
};
Expand Down

0 comments on commit ea5f182

Please sign in to comment.