Skip to content

Instantly share code, notes, and snippets.

@nijikokun
Created March 7, 2015 01:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nijikokun/b004ecb377676c32673f to your computer and use it in GitHub Desktop.
Save nijikokun/b004ecb377676c32673f to your computer and use it in GitHub Desktop.
Mongoose Passport LocalStrategy Plugin, supports PBKDF2, BCrypt, MD5
var LocalStrategy = require('passport-local').Strategy;
var crypto = require('crypto');
var bcrypt = require('bcrypt');
var util = require('util');
function BadRequestError (message) {
Error.call(this);
Error.captureStackTrace(this, arguments.callee);
this.name = 'BadRequestError';
this.message = message || null;
}
util.inherits(BadRequestError, Error);
module.exports = function (schema, options) {
var schemaFields = {};
// Ensure options is an object
options = options || {};
// Field names
options.usernameField = options.usernameField || 'username';
options.methodField = options.methodField || 'hashMethod';
options.hashField = options.hashField || 'hash';
options.saltField = options.saltField || 'salt';
// Encryption option, ensure casing
options.method = options.method || 'bcrypt';
options.method = options.method.toLowerCase();
// BCrypt Options
options.saltWorkFactor = options.saltWorkFactor || 10;
// PBKDF2 options
options.encoding = options.encoding || 'hex';
options.saltLength = options.saltLength || 32;
options.hashLength = options.hashLength || 512;
options.hashIterations = options.hashIterations || 64000;
// Convert username to lowercase?
options.usernameLowerCase = options.usernameLowerCase || false;
// Error messages
options.incorrectPasswordError = options.incorrectPasswordError || 'Invalid password given';
options.incorrectUsernameError = options.incorrectUsernameError || 'Invalid %s given';
options.missingUsernameError = options.missingUsernameError || '%s is required';
options.missingPasswordError = options.missingPasswordError || 'Password is required';
options.userExistsError = options.userExistsError || 'User already exists with %s %s';
options.noSaltValueStoredError = options.noSaltValueStoredError || 'Authentication not possible. No salt value stored!';
options.attemptTooSoonError = options.attemptTooSoonError || 'Login attempted too soon after previous attempt';
// Prevent brute forcing?
if (options.limitAttempts) {
options.lastLoginField = options.lastLoginField || 'last';
options.attemptsField = options.attemptsField || 'attempts';
options.interval = options.interval || 100; // in ms
schemaFields[options.attemptsField] = {
type: Number, default: 0
};
schemaFields[options.lastLoginField] = {
type: Date, default: Date.now
};
}
// Ensure username field exists
if (!schema.path(options.usernameField)) {
schemaFields[options.usernameField] = String;
}
// Setup hashing fields
schemaFields[options.hashField] = String;
schemaFields[options.saltField] = String;
schemaFields[options.methodField] = String;
// Save fields
schema.add(schemaFields);
// Ensure username is lowercase on save when flag is set
schema.pre('save', function (next) {
// if specified, convert the username to lowercase
if (options.usernameLowerCase) {
this[options.usernameField] = this[options.usernameField].toLowerCase();
}
next();
});
/**
* Asynchronous method to set a user's password hash and salt.
*
* @param {String} password Account password
* @param {Function} cb Callback
*/
schema.methods.setPassword = function (password, cb) {
if (!password) {
return cb(new BadRequestError(options.missingPasswordError));
}
var self = this;
switch (options.method) {
case "bcrypt":
bcrypt.genSalt(options.saltWorkFactor, function (err, salt) {
if (err) {
return cb(err);
}
bcrypt.hash(password, salt, function (err, hash) {
if (err) {
return cb(err);
}
self.set(options.hashField, hash);
self.set(options.methodField, 'bcrypt');
cb(null, self);
});
});
break;
case "pbkdf2":
crypto.randomBytes(options.saltLength, function (err, buf) {
if (err) {
return cb(err);
}
var salt = buf.toString(options.encoding);
crypto.pbkdf2(password, salt, options.hashIterations, options.hashLength, function (err, hashRaw) {
if (err) {
return cb(err);
}
self.set(options.hashField, new Buffer(hashRaw, 'binary').toString(options.encoding));
self.set(options.saltField, salt);
self.set(options.methodField, 'pbkdf2');
cb(null, self);
});
});
break;
}
};
/**
* Compares stored hash against md5 checksum
*
* Returns
* - Error
* - isMatch (True / False)
*
* @param {String} password Specified password to compare
* @param {Function} cb Callback
*/
schema.methods.comparePasswordMD5 = function (password, cb) {
return cb(null, crypto.createHash('md5').update(password).digest("hex") === this.get(options.hashField));
};
/**
* Compares stored hash against bcrypt hashing
*
* Returns
* - Error
* - isMatch (True / False)
*
* @param {String} password Specified password to compare
* @param {Function} cb Callback
*/
schema.methods.comparePasswordBcrypt = function (password, cb) {
bcrypt.compare(password, this.get(options.hashField), cb);
};
/**
* Compare password and stored hash + salt using PBKDF2
*
* Returns
* - Error
* - isMatch (True / False)
*
* @param {String} password Specified password to compare
* @param {Function} cb Callback
*/
schema.methods.comparePasswordPbkdf2 = function (password, cb) {
var self = this;
crypto.pbkdf2(password, this.get(options.saltField), options.hashIterations, options.hashLength, function (err, hashRaw) {
if (err) {
return cb(err);
}
if (new Buffer(hashRaw, 'binary').toString(options.encoding) === self.get(options.hashField)) {
return cb(null, true);
}
return cb(null, false);
});
};
/**
* Authenticate current user with brute forcing prevention when enabled.
*
* @param {String} password Specified password to compare
* @param {Function} cb Callback
*/
schema.methods.authenticate = function (password, cb) {
var self = this;
var method = this.get(options.methodField) || options.method;
// Check whether login attempt is too soon after the previous attempt
if (options.limitAttempts && (Date.now() - this.get(options.lastLoginField) < Math.pow(options.interval, this.get(options.attemptsField) + 1))) {
this.set(options.lastLoginField, Date.now());
self.save();
return cb(null, false, { message: options.attemptTooSoonError });
}
// Check salt value exists
if (!this.get(options.saltField)) {
return cb(null, false, { message: options.noSaltValueStoredError });
}
// On Success
function success () {
if (options.limitAttempts) {
self.set(options.lastLoginField, Date.now());
self.set(options.attemptsField, 0);
self.save();
}
return cb(null, self);
}
// On Failure
function failure () {
if (options.limitAttempts) {
self.set(options.lastLoginField, Date.now());
self.set(options.attemptsField, self.get(options.attemptsField) + 1);
self.save();
}
return cb(null, false, { message: options.incorrectPasswordError });
}
// Generic comparison handler
function generic (err, matches) {
if (err) {
return cb(err);
}
return matches ? success() : failure();
}
switch (method.toLowerCase()) {
case "bcrypt":
self.comparePasswordBcrypt(password, generic);
break;
case "pbkdf2":
self.comparePasswordPbkdf2(password, generic);
break;
case "md5":
self.comparePasswordMD5(password, generic);
break;
}
};
/**
* Generates a function to be used in Passport's LocalStrategy
*
* @return {Function}
*/
schema.statics.authenticate = function () {
var self = this;
return function (username, password, cb) {
self.findByUsername(username, function (err, user) {
if (err) {
return cb(err);
}
if (user) {
return user.authenticate(password, cb);
} else {
return cb(null, false, {
message: util.format(options.incorrectUsernameError, options.usernameField)
});
}
});
};
};
/**
* Generates a function to be used by Passport to serialize users into the session
*
* @return {Function}
*/
schema.statics.serializeUser = function () {
return function (user, cb) {
cb(null, user.get(options.usernameField));
};
};
/**
* Generates a function to be used by Passport to deserialize users into the session
*
* @return {Function}
*/
schema.statics.deserializeUser = function () {
var self = this;
return function (username, cb) {
self.findByUsername(username, cb);
};
};
/**
* Convenience method to register a new user instance with a given password. Checks if username is unique.
*
* @param {String} user Account Username
* @param {String} password Account password
* @param {Function} cb Callback
*/
schema.statics.register = function (user, password, cb) {
var self = this;
// Instantiate user if the user isn't already an instance
if (!(user instanceof this)) {
user = new this(user);
}
// Ensure user has the username field
if (!user.get(options.usernameField)) {
return cb(new BadRequestError(util.format(options.missingUsernameError, options.usernameField)));
}
self.findByUsername(user.get(options.usernameField), function (err, existingUser) {
if (err) {
return cb(err);
}
if (existingUser) {
return cb(new BadRequestError(
util.format(options.userExistsError, options.usernameField, user.get(options.usernameField))
));
}
user.setPassword(password, function(err, user) {
if (err) {
return cb(err);
}
user.save(function(err) {
if (err) {
return cb(err);
}
cb(null, user);
});
});
});
};
/**
* Convenience method to find a user instance by it's unique username.
*
* @param {String} username Account username
* @param {Function} cb Callback
* @return {Query|void} When cb is not defined, the query instance is returned.
*/
schema.statics.findByUsername = function (username, cb) {
var queryParameters = {};
var query;
// if specified, convert the username to lowercase
if (username !== undefined && options.usernameLowerCase) {
username = username.toLowerCase();
}
queryParameters[options.usernameField] = username;
query = this.findOne(queryParameters);
if (options.selectFields) {
query.select(options.selectFields);
}
if (options.populateFields) {
query.populate(options.populateFields);
}
if (cb) {
query.exec(cb);
} else {
return query;
}
};
/**
* Creates a configured passport-local LocalStrategy instance that can be used in passport.
*
* @return {LocalStrategy}
*/
schema.statics.createStrategy = function () {
return new LocalStrategy(options, this.authenticate());
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment