UNPKG

@inspire-platform/sails-hook-auth

Version:

Passport-based User Authentication system for sails.js applications.

312 lines (262 loc) 8.85 kB
var _ = require('lodash'); var bcrypt = require('bcryptjs'); var OwaspPST = require('@inspire-platform/owasp-password-strength-test'); var SAError = require('../../lib/error/SAError.js'); var SAPassportLockedError = require('../../lib/error/SAPassportLockedError.js'); /** * Hash a passport password. * * @param {Object} password * @param {Function} next */ function hashPassword (passport, next) { var config = sails.config.auth.bcrypt; var salt = config.salt || config.rounds; if (passport.password) { bcrypt.hash(passport.password, salt, function (err, hash) { if (err) { delete passport.password; sails.log.error(err); throw err; } passport.password = hash; next(null, passport); }); } else { next(null, passport); } } /** * Passport Model * * The Passport model handles associating authenticators with users. An authen- * ticator can be either local (password) or third-party (provider). A single * user can have multiple passports, allowing them to connect and use several * third-party strategies in optional conjunction with a password. * * Since an application will only need to authenticate a user once per session, * it makes sense to encapsulate the data specific to the authentication process * in a model of its own. This allows us to keep the session itself as light- * weight as possible as the application only needs to serialize and deserialize * the user, but not the authentication data, to and from the session. */ var Passport = { primaryKey: 'id', attributes: { // Primary Key id: { type: 'number', unique: true, autoIncrement: true }, // Required field: Protocol // // Defines the protocol to use for the passport. When employing the local // strategy, the protocol will be set to 'local'. When using a third-party // strategy, the protocol will be set to the standard used by the third- // party service (e.g. 'oauth', 'oauth2', 'openid'). protocol: { type: 'string', required: true, regex: /^[a-z0-9]+$/i }, // Local field: Password // // When the local strategy is employed, a password will be used as the // means of authentication along with either a username or an email. password: { type: 'string', minLength: 8 }, // accessToken is used to authenticate API requests. it is generated when a // passport (with protocol 'local') is created for a user. accessToken: { type: 'string' }, // number of failed login attempts since last successful local login lockoutAttempts: {type: 'number', defaultsTo: 0}, // timestamp that lockout started lockoutSince: {type: 'number', allowNull: true}, // Provider fields: Provider, identifer and tokens // // "provider" is the name of the third-party auth service in all lowercase // (e.g. 'github', 'facebook') whereas "identifier" is a provider-specific // key, typically an ID. These two fields are used as the main means of // identifying a passport and tying it to a local user. // // The "tokens" field is a JSON object used in the case of the OAuth stan- // dards. When using OAuth 1.0, a `token` as well as a `tokenSecret` will // be issued by the provider. In the case of OAuth 2.0, an `accessToken` // and a `refreshToken` will be issued. provider : { type: 'string', regex: /^[a-z0-9]+(-[a-z0-9]+)*$/i }, identifier : { type: 'string' }, tokens : { type: 'json' }, // Associations // // Associate every passport with one, and only one, user. This requires an // adapter compatible with associations. // // For more information on associations in Waterline, check out: // https://github.com/balderdashy/waterline user: { model: 'User', required: true }, }, /** * Validate password used by the local strategy. * * @param {string} password The password to validate * @param {Function} next */ validatePassword: function (passport, password, next) { // get password config var config = sails.config.auth.password; // lockout disabled? if (config.lockout.enable !== true) { // yes, just check the password return bcrypt.compare(password, passport.password, next); } this .isLocked(passport) .then(() => { // not locked out OR lock expired, need to check password return bcrypt.compare(password, passport.password); }) .then((result) => { let data = { lockoutAttempts: 0, lockoutSince: null }; // password is valid? if (result !== true) { // invalid password, :( // coming off a lockout, and still not getting it right? if (passport.lockoutSince) { // yes... reset fields to give more attempts data.lockoutAttempts = 1; data.lockoutSince = null; } else { // increment failed attempts. data.lockoutAttempts = passport.lockoutAttempts + 1 // lock them out? if (config.lockout.attempts <= data.lockoutAttempts) { data.lockoutSince = new Date(); } } } // update it var updatePassport = this .update({id: passport.id}, data) .fetch() .then((passports) => { return passports[0]; }); return Promise.all([ result, updatePassport ]); }) .then((results) => { var authenticated = results[0]; var passport = results[1]; // check if this attempt resulted in a lock return this .isLocked(passport) .then(() => { // not locked return next(null, authenticated); }); }) .catch(next); }, isLocked: function (passport) { // get password config var config = sails.config.auth.password; return new Promise((resolve, reject) => { // is passport locked out? if (passport.lockoutSince) { try { var dateNow = new Date(); var lockoutSince = new Date(passport.lockoutSince); var lockoutExpires = new Date(lockoutSince.getTime() + config.lockout.wait * 1000); } catch (e) { return reject(e); } // lockout still in effect? if (dateNow < lockoutExpires) { // still locked out var error = new SAPassportLockedError({ since: lockoutSince, expires: lockoutExpires, attempts: passport.lockoutAttempts, }); return reject(error); } } return resolve(); }); }, /** * Check password strength. * * @param {string} password * @param {Object} user */ checkPasswordStrength: function (password, user) { return new Promise((resolve, reject) => { // get password config var config = sails.config.auth.password; var owasp = new OwaspPST(config.owasp); // any user properties to scan for? if (config.userAttributeScan.length) { config.userAttributeScan.forEach((scan) => { owasp.tests.required.push((password) => { if (true === _.has(user, scan.attribute)) { var re = new RegExp(user[scan.attribute], 'i'); if (true === re.test(password)) { return `Password must not contain the value of the ${scan.description}.`; } } }); }); } var result = owasp.test(password); if (result.errors.length === 0) { return resolve(true); } else { let error = new Error(); error.invalidAttributes = { password: result.errors.map((msg) => { return { rule: 'owasp', message: msg } }) }; return reject(new SAError({message: 'Password is not strong enough', name: 'UsageError', originalError: error})); } }); }, /** * Callback to be run before creating a Passport. * * @param {Object} passport The soon-to-be-created Passport * @param {Function} next */ beforeCreate: function (passport, next) { hashPassword(passport, next); }, /** * Callback to be run before updating a Passport. * * @param {Object} passport Values to be updated * @param {Function} next */ beforeUpdate: function (passport, next) { // reset lockout fields if password is being changed if (passport.password) { passport.lockoutAttempts = 0; passport.lockoutSince = null; } hashPassword(passport, next); } }; module.exports = Passport;