@house-agency/brewsession
Version:
The Brewery Session Manager
332 lines (299 loc) • 8.13 kB
JavaScript
const _ = require('lodash');
const conf = require('@house-agency/brewtils/config');
const crypto = require('crypto');
const def = require('@house-agency/brewtils/catch-default');
const format = require('util').format;
const keyvalue = require('@house-agency/brewstore/keyvalue');
const log = require('@house-agency/brewtils/log');
const q = require('q');
const sha = require('jssha');
/**
* Store session oriented data in key/value storage.
* It uses storage keys with pattern session:<session-token>:<id>
*
* @param {string} token Session token
* @param {string} id Data identifier key
* @param {string} method Method to use for get/set
*
* @return {object} Promise with db results
*/
function data(token, id, method) {
const args = _.toArray(arguments).slice(3);
const key = format('sessions:%s:%s', token, id);
return keyvalue.run(key, method, args);
}
/**
* Parses provided api key
*
* @param {string} key Api key as it's stored in database
*
* @returns {object} { time: <generation-time>, token: <key-token> }
*/
function parse_apikey(key) {
const arr = key.split(':');
if (arr.length === 2) {
return {
time: parseInt(arr[0], 10),
token: arr[1]
};
}
throw new Error('Could not parse api key');
}
/**
* Get current api keys from database
*
* @return {array} Keys in an array - in a promise
*/
function get_apikeys() {
return keyvalue.run('api-keys', 'lrange', [0, -1]);
}
/**
* Matches provided key against config and keys stored in db
*
* @param {string} key
*
* @return {string} key if success - in a promise, throws error on fail
*/
function match_apikey(key) {
return conf('api.key', null)
.then(conf_key => {
if (conf_key !== null && conf_key === key) {
return key;
}
return get_apikeys()
.then(db_keys => {
if (_.indexOf(db_keys, key) > -1) {
return key;
}
throw new Error('Invalid key');
});
});
}
/**
* Removes specified api key from db
*
* @param {string} key
* @return {null} Empty promise
*/
function remove_apikey(key) {
return keyvalue.run('api-keys', 'lrem', [0, key]);
}
/**
* Generates a new api key, stores it in the database
* and returns it.
*
* It will only create one new key since last key generation
* plus api.generate time in config. Removes keys older than
* api.remove in config.
*
* Keys are stored in a key array called api-keys with pattern
* <generation-time>:<key-token>
*
* @return {string} Key in a promise
*/
function generate_apikey() {
return q.all([
// Get config data and keys
conf('api.generate'),
conf('api.remove'),
get_apikeys()
.then(function (keys) {
if (keys.length > 0) {
return keys;
}
throw new Error('Not found');
})
])
.spread((time_generate, time_remove, keys) => {
// Validate keys by dates and remove keys that aren't valid.
return _.reduce(keys, (queue, key) => {
const time = parse_apikey(key).time;
if (time + time_remove < (new Date()).getTime()) {
// Not valid - remove
return queue
.then(() => {
return remove_apikey(key);
});
}
return queue;
}, q())
.then(() => {
// If last key was generated after api.generate time
// has passed, throw not-found error which'll trigger
// a new key generation.
const key = _.last(keys);
const time = parse_apikey(key).time;
if (time + time_generate < (new Date()).getTime()) {
throw new Error('Not found');
}
return key;
});
})
.catch(def.not_found(() => {
return q.ninvoke(crypto, 'randomBytes', 256)
.then(random_buffer => {
const sha224 = new sha('SHA-224', 'TEXT');
sha224.update(random_buffer.toString());
return sha224.getHash('HEX');
})
.then(hash => {
const key = format(
'%d:%s',
(new Date()).getTime(),
hash
);
return keyvalue.run('api-keys', 'rpush', [key])
.then(() => {
log('debug', 'Generated api key', key);
return key;
});
});
}));
}
/**
* Generates a session token by api key
*
* @param {string} key Api key
*
* @return {string} Session token in a promise
*/
function generate_token(key) {
return q.all([
q.ninvoke(crypto, 'randomBytes', 256),
new sha('SHA-224', 'TEXT')
])
.spread((random_buffer, sha224) => {
sha224.update(format(
'%s%s%s',
key,
random_buffer.toString(),
(new Date()).getTime()
));
return sha224.getHash('HEX');
});
}
/**
* Get the validation time for session by session token
*
* @param {string} token Session token
*
* @return {object} Promise with timestamp in milliseconds and a extras string
*/
function get_valid(token) {
return q.all([
data(token, 'valid', 'get'),
data(token, 'extras', 'get')
])
.spread((time, extras) => {
return {
time: time,
extras: extras
};
});
}
/**
* Set the validation time for session by session token
*
* @param {string} token Session token
* @param {number} time Timestamp in milliseconds
*
* @return {object} Empty promise
*/
function set_valid(token, time) {
return data(token, 'valid', 'set', time);
}
/**
* Creates a session by an API-key and returns a session token
* Or fails of key is not valid.
*
* @param {string} key API-key
* @param {...*} args Extra arguments to limit validation: user-agent, ip, whatever
*
* @return {string} Promise with session token string
*/
function create(key) {
return match_apikey(key)
.then(key => {
return generate_token(key);
})
.then(token => {
const sha224 = new sha('SHA-224', 'TEXT');
sha224.update(_.toArray(arguments).slice(1).join(''));
return q.all([
set_valid(token, (new Date()).getTime()),
data(token, 'extras', 'set', sha224.getHash('HEX'))
])
.then(() => {
return token;
});
});
}
/**
* Verifies session by it's token and if it's still valid
*
* @param {string} token Session token
* @param {...*} args Extra arguments to limit validation: user-agent, ip, whatever
*
* @return {object} Promise with session token string or fail
*/
function verify(token) {
const sha224 = new sha('SHA-224', 'TEXT');
sha224.update(_.toArray(arguments).slice(1).join(''));
return q.all([
conf('api.valid'),
get_valid(token),
sha224.getHash('HEX')
])
.spread((valid, session, extras) => {
if (
session.time + parseInt(valid, 10) > (new Date()).getTime() &&
session.extras === extras
) {
return token;
}
throw new Error('Invalid session token');
});
}
/**
* Exceeds the expiration time for session by token
*
* @param {string} token Session token
*
* @return {object} Promise with session token
*/
function exceed(token) {
return set_valid(token, (new Date()).getTime())
.then(() => {
return token;
});
}
/**
* Removes session by token
*
* @param {string} token Session token
*
* @return {object} Empty promise
*/
function remove(token) {
return data(token, '*', 'keys')
.then(keys => {
return q.all(_.map(keys, function (key) {
return keyvalue.run(key, 'del');
}));
});
}
module.exports = _.reduce([
generate_apikey,
get_apikeys,
match_apikey,
remove_apikey,
create,
verify,
exceed,
remove,
get_valid,
data
], function (exp, func) {
exp[func.name] = func;
return exp;
}, {});