UNPKG

passwordless-redisstore-bcryptjs

Version:

Redis TokenStore for Passwordless (with pure js bcrypt)

238 lines (218 loc) 6.81 kB
'use strict'; var util = require('util'); var bcrypt = require('bcryptjs'); var TokenStore = require('passwordless-tokenstore'); var redis = require('redis'); var async = require('async'); /** * Constructor of RedisStore * @param {String} connection URI as defined by the MongoDB specification. Please * check the documentation for details: * http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html * @param {Object} [options] Combines both the options for the MongoClient as well * as the options for RedisStore. For the MongoClient options please refer back to * the documentation. RedisStore understands the following options: * (1) { mongostore: { collection: string }} to change the name of the collection * being used. Defaults to: 'passwordless-token' * @constructor */ function RedisStore(port, host, options) { this._options = options || {}; this._options.redisstore = this._options.redisstore || {}; if(this._options.redisstore.database && !isNumber(this._options.redisstore.database)) { throw new Error('database has to be a number (if provided at all)'); } else if(this._options.redisstore.database) { this._database = this._options.redisstore.database; } else { this._database = 0; } this._tokenKey = this._options.redisstore.tokenkey || 'pwdless:'; delete this._options.redisstore; this._client = redis.createClient(port, host, this._options); } util.inherits(RedisStore, TokenStore); /** * Checks if the provided token / user id combination exists and is * valid in terms of time-to-live. If yes, the method provides the * the stored referrer URL if any. * @param {String} token to be authenticated * @param {String} uid Unique identifier of an user * @param {Function} callback in the format (error, valid, referrer). * In case of error, error will provide details, valid will be false and * referrer will be null. If the token / uid combination was not found * found, valid will be false and all else null. Otherwise, valid will * be true, referrer will (if provided when the token was stored) the * original URL requested and error will be null. */ RedisStore.prototype.authenticate = function(token, uid, callback) { if(!token || !uid || !callback) { throw new Error('TokenStore:authenticate called with invalid parameters'); } var self = this; self._select(function(err) { if(err) { return callback(err, false, null); } var key = self._tokenKey + uid; self._client.hgetall(key, function(err, obj) { if(err) { return callback(err, false, null); } else if(!obj) { return callback(null, false, null); } else if(Date.now() > obj.ttl) { callback(null, false, null); } else { bcrypt.compare(token, obj.token, function(err, res) { if(err) { callback(err, false, null); } else if(res) { callback(null, true, obj.origin); } else { callback(null, false, null); } }); } }); }) }; /** * Stores a new token / user ID combination or updates the token of an * existing user ID if that ID already exists. Hence, a user can only * have one valid token at a time * @param {String} token Token that allows authentication of _uid_ * @param {String} uid Unique identifier of an user * @param {Number} msToLive Validity of the token in ms * @param {String} originUrl Originally requested URL or null * @param {Function} callback Called with callback(error) in case of an * error or as callback() if the token was successully stored / updated */ RedisStore.prototype.storeOrUpdate = function(token, uid, msToLive, originUrl, callback) { if(!token || !uid || !msToLive || !callback || !isNumber(msToLive)) { throw new Error('TokenStore:storeOrUpdate called with invalid parameters'); } var self = this; self._select(function(err) { if(err) { return callback(err, false, null); } bcrypt.hash(token, 10, function(err, hashedToken) { if(err) { return callback(err); } var key = self._tokenKey + uid; self._client.hmset(key, { token: hashedToken, origin: originUrl, ttl: (Date.now() + msToLive) }, function(err, res) { if(!err) { msToLive = Math.ceil(msToLive / 1000); self._client.expire(key, msToLive, function(err, res) { if(err) callback(err); else callback(); }) } else { callback(err); } }); }); }) } /** * Invalidates and removes a user and the linked token * @param {String} user ID * @param {Function} callback called with callback(error) in case of an * error or as callback() if the uid was successully invalidated */ RedisStore.prototype.invalidateUser = function(uid, callback) { if(!uid || !callback) { throw new Error('TokenStore:invalidateUser called with invalid parameters'); } var self = this; self._select(function(err) { if(err) { return callback(err, false, null); } var key = self._tokenKey + uid; self._client.del(key, function(err) { if(err) callback(err); else callback(); }); }) } /** * Removes and invalidates all token * @param {Function} callback Called with callback(error) in case of an * error or as callback() otherwise */ RedisStore.prototype.clear = function(callback) { if(!callback) { throw new Error('TokenStore:clear called with invalid parameters'); } var self = this; self._select(function(err) { if(err) { return callback(err, false, null); } var pattern = self._tokenKey + '*'; self._client.keys(pattern, function(err, matches) { if(err) { callback(err); } else if(matches.length > 0) { async.each(matches, function(match, matchCallback) { self._client.del(match, matchCallback); }, callback); } else { callback(); } }); }) } /** * Number of tokens stored (no matter the validity) * @param {Function} callback Called with callback(null, count) in case * of success or with callback(error) in case of an error */ RedisStore.prototype.length = function(callback) { if(!callback) { throw new Error('TokenStore:length called with invalid parameters'); } var self = this; self._select(function(err) { if(err) { return callback(err, false, null); } var pattern = self._tokenKey + '*'; self._client.keys(pattern, function(err, matches) { if(err) { callback(err); } else { callback(null, matches.length); } }); }) } RedisStore.prototype._select = function(callback) { var self = this; if(self._selected) { callback(); } else { self._client.select(self._database, function(err, res) { if(err) { callback(err); } else { self._selected = true; callback(); } }); } } function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); } module.exports = RedisStore;