UNPKG

multi-auth

Version:

An authentication module that supports passwords, email auth, and 2-step auth

456 lines (379 loc) 11 kB
var utils = require('./utils'); var Promise = require('promise-es6').Promise; var MemoryStore = require('./key-stores/memory'); var RedisStore = require('./key-stores/redis'); var MongoStore = require('./key-stores/mongo'); // "Constants" representing the different reasons for sending emails var EMAIL = { EMAIL_AUTH: 'emailAuth', TWO_STEP_AUTH: 'twoStepAuth', PASSWORD_RESET: 'passwordReset', EMAIL_CONFIRM: 'emailConfirm' }; // "Constants" representing the different reasons for sending SMS messages var SMS = { TWO_STEP_AUTH: 'twoStepAuth' }; // // Authentication class // var Auth = exports = module.exports = function(opts) { // The keystore to use for email/two step auth this.keyStore = opts.keyStore || new MemoryStore(); // List of the allowed auth methods this.authMethods = opts.authMethods || [ 'password', 'email', 'twostep-email', 'twostep-sms' ]; // Map of user-defined actions this.actions = [ // // Send an email to a user // // @param {user} a user object {id,authMethod,[email],[password],[phone]} // @param {template} the type of email to send (will be one of "emailAuth", "twoStepAuth", "passwordReset", "emailConfirm") // @param {data} data relavent to the message, eg. {user,token} // @param {promise} a promise to resolve/reject // @result void // 'sendEmail', // // Semd am SMS message to a user // // @param {user} a user object containing auth-related properties // @param {template} the type of message to send (will be one of "twoStepAuth") // @param {data} data relavent to the message, eg. {user,token} // @param {promise} a promise to resolve/reject // @result void // 'sendSMS', // // Fetch an object with user data by the user's ID // // @param {userId} the user's ID // @param {promise} a promise to resolve/reject // @result {id,authMethod,[email],[password],[phone]} // 'fetchUserById', // // Fetch an object with user data by a name value (username or email) // // @param {name} the user name value // @param {promise} a promise to resolve/reject // @result {id,name,email,authMethod} // 'fetchUserByName', // // Check if the given password is valid for the given user // // @param {user} a user object {id,authMethod,[email],[password],[phone]} // @param {password} the password value to check // @param {promise} a promise to resolve/reject // @result boolean // 'checkPassword', // // Mark a user's email address as valid and confirmed // // @param {userId} a user ID value // @param {promise} a promise to resolve/reject // @result void // 'confirmEmail', // // Update a user's password to the given value // // @param {userId} a user ID value // @param {password} the new password to use // @param {promise} // @result void // 'updatePassword' ] .reduce(function(mem, key) { mem[key] = opts[key] || function() { }; return mem; }, { }); }; // ------------------------------------------------------------- // // Defines an action // // @param {action} the name of the action // @param {func} the function to define // @return this // Auth.prototype.define = function(action, func) { // Allow passing in an object with multiple actions defined if (typeof action === 'object' && action) { Object.keys(action).forEach(function(key) { this.define(key, action[key]); }.bind(this)); } this.actions[action] = func; return this; }; // // Take a user defined action // // @param {action} the name of the function to call // @param {...} arguments to pass to the function // @return promise // Auth.prototype.action = function(action) { var deferred = utils.defer(); var args = Array.prototype.slice.call(arguments, 1); args.push(deferred); this.actions[action].apply(this, args); return deferred.promise; }; // ------------------------------------------------------------- // // Express middleware to simplify setup // // @param {opts} middleware options // @return void // Auth.prototype.express = function(opts) { var auth = this; var route = opts.route || '/auth'; var afterAuth = opts.afterAuth || function() { }; var opening = new RegExp('^' + route); return function(req, res, next) { // Normalize the pathname for easier matching var pathname = req.pathname.replace(opening, '/auth'); // Combine the method and path together and match as a single unit switch (req.method.toUpperCase() + ' ' + pathname) { // // Step One: // Fetches user info based on a username/email, check the selected // auth method, and start authenticating // case 'POST /auth': return auth.startAuthentication(req.body.username) .then(function(user) { // Authentication complete, let the afterAuth method handle it if (user && typeof user === 'object') { return afterAuth(user, req, res, next); } // Authentication is still in progress (eg. two step auth) if (user) { return res.send(202, { message: 'Authentication message sent' }); } // Authentication has failed res.send(401, { message: 'Authentication failed' }) }) .catch(function(err) { res.send(500, err); }); break; // // Any non-matching request should simply pass through // default: next(); break; } }; }; // ------------------------------------------------------------- // // Sends a confirmation email to a user // // @param {userId} the user ID of the user to confirm // @return promise // Auth.prototype.confirmEmailStepOne = function(userId) { var self = this; var user, key; return Promise.all([ this.action('fetchUserById', userId), this.keyStore.generateKey() ]) .then(function(results) { user = results[0]; key = results[1]; return self.keyStore.registerKey(key, user.id, EMAIL.EMAIL_CONFIRM); }) .then(function() { return self.action('sendEmail', user, EMAIL.EMAIL_CONFIRM, { user: user, token: key }); }); }; // // Checks that a confirmation token is valid, and marks the user's email as confirmed // // @param {key} the confirmation key // @return promise // Auth.prototype.confirmEmailStepTwo = function(key) { var self = this; return this.keyStore.find(key) .then(function(found) { console.log(found); if (! found || found.type !== EMAIL.EMAIL_CONFIRM) { return Promise.reject('Invalid email confirmation token'); } return self.action('confirmEmail', found.userId) }) .then(function() { return self.keyStore.removeKey(key); }); }; // ------------------------------------------------------------- // // Starts the authentication process for a user by looking up the user data // and selecting the correct authentication method // // @param {data} a data object containing auth credentials {username,[password]} // @return promise // Auth.prototype.startAuthentication = function(data) { var self = this; var key; return this.action('fetchUserByName', data.username) .then(function(user) { if (! user) { throw new Error('User not found'); } switch (user.authMethod) { // // Password-only auth // case 'password': if (! data.password) { throw new Error('Selected authentication method is "password", but no password was given'); } return self.action('checkPassword', user, data.password) .then(function(result) { if (result) { return user; } return false; }); break; // // Email-only auth // case 'email': return self.keyStore.generateKey({ generator: utils.uid }) .then(function(_key) { key = _key; }) .then(function() { self.keyStore.registerKey(key, user.id, EMAIL.EMAIL_AUTH) }) .then(function() { return self.action('sendEmail', user, EMAIL.EMAIL_AUTH, { key: key, user: user }); }); break; // // Two-step password/email auth // case 'twostep-email': if (! data.password) { throw new Error('Selected authentication method is "twostep-email", but no password was given'); } return self.action('checkPassword', user, data.password) .then(function(result) { if (result) { return self.keyStore.generateKey({ generator: utils.uid }) .then(function(_key) { key = _key; }) .then(function() { return self.keyStore.registerKey(key, user.id, EMAIL.TWO_STEP_AUTH); }) .then(function() { return self.action('sendEmail', user, EMAIL.TWO_STEP_AUTH, { key: key, user: user }); }); } return false; }); break; // // Two-step password/sms auth // case 'twostep-sms': if (! data.password) { throw new Error('Selected authentication method is "twostep-sms", but no password was given'); } return self.action('checkPassword', user, data.password) .then(function(result) { if (result) { return self.keyStore.generateKey({ generator: utils.uid }) .then(function(_key) { key = _key; }) .then(function() { return self.keyStore.registerKey(key, user.id, SMS.TWO_STEP_AUTH); }) .then(function() { return self.action('sendEmail', user, SMS.TWO_STEP_AUTH, { key: key, user: user }); }); } return false; }); break; // // Invalid auth method // default: throw new Error('Invalid authentication method selected'); break; } }); }; // ------------------------------------------------------------- // // Starts the two-step authentication process // // @param {username} the user to authenticate for // @param {password} the password to attempt to log in with // @return promise // Auth.prototype.passwordAuth = function(username, password) { var self = this; var user; return this.action('fetchUserByName', username) .then(function(_user) { user = _user; }) .then(function() { return self.action('checkPassword', user.id, password); }) .then(function(result) { if (result) { return user; } return false; }); }; // // Step two of two-step authentication, takes a confirmation code // // @param {code} the authentication code // @return promise // Auth.prototype.stepTwo = function(code) { var self = this; var user; return this.keyStore.find(code) .then(function(code) { if (! code) { throw new Error('Invalid confirmation code'); } if (code.type !== EMAIL.TWO_STEP_AUTH) { throw new Error('Invalid use for code'); } return self.action('fetchUserById', code.userId); }) .then(function(_user) { user = _user; }) .then(function() { return self.keyStore.removeKey(code); }) .then(function() { return user; }); };