I'm using a pre-save hook with bcrypt to encrypt passwords on the system. It works fine when creating or changing a password. The problem is that it seems to re-encrypt the password every time I change and save a different field, for example e-mail.
Probably easier to explain with code. Here's the model:
const UserSchema = new Schema({
email: {
type: String,
required: true,
lowercase: true,
unique: true,
trim: true
},
password: {
type: String,
required: true
}
})
And the hook:
UserSchema.pre('save', function(next){
const user = this;
console.log(user);
bcrypt.genSalt(10, function(err, salt){
if (err){ return next(err) }
bcrypt.hash(user.password, salt, null, function(err, hash){
if(err){return next(err)}
user.password = hash;
next();
})
})
});
And here's my code to update the e-mail address:
module.exports = function(req, res){
User.findOne({ _id: req.body.user}, function(err, doc){
if(err){
console.log(err);
return;
}
doc.email = req.body.data;
doc.save(function(err, returnData){
if (err){
console.log(err);
return;
}
res.send(returnData);
})
})
}
So when I call doc.save in the final example, it updates the e-mail address as intended but it also re-encrypts the password, meaning if the user then logs out, they can't log back in again.
Can anyone help with how to get around this?
Try this:
UserSchema.pre('save', function(next){
if (!this.isModified('password')) return next();
const user = this;
bcrypt.genSalt(10, function(err, salt){
if (err){ return next(err) }
bcrypt.hash(user.password, salt, null, function(err, hash){
if(err){return next(err)}
user.password = hash;
next();
})
})
});
OK, I managed to figure it out - just needed a little bit of conditional logic in the pre-save hook:
UserSchema.pre('save', function(next){
if(!this.isModified('password')){
return next();
} // Adding this statement solved the problem!!
const user = this;
bcrypt.genSalt(10, function(err, salt){
if (err){ return next(err) }
bcrypt.hash(user.password, salt, null, function(err, hash){
if(err){return next(err)}
user.password = hash;
next();
})
})
});
Related
I made a login with bcrypt.
I also made a page where users can edit their information, like their bio etc.
Each time an user edit his bio on this page the hash from bcrypt change, which is normal i suppose, but the user login back, the password is wrong...
I used the same model for mongoDb for the user when he log in and when he edit his data.
I started node.js recently so I apologize if my question is stupid,,,
The controller code with the Post :
app.post('/settings-user', mid.requiresLogin, function(req, res, next){
User.findById(req.session.userId, function (err, user) {
// todo: don't forget to handle err
if (!user) {
return res.redirect('/edit');
}
// good idea to trim
var bio = req.body.bio.trim();
// validate
if (!bio) { // simplified: '' is a falsey
req.flash('error', 'One or more fields are empty');
return res.redirect('/settings-user'); // modified
}
// no need for else since you are returning early ^
user.bio = bio;
// don't forget to save!
user.save(function (err) {
// todo: don't forget to handle err
res.redirect('/settings-user/');
});
});
});
The User model :
app.post('/settings-user', mid.requiresLogin, function(req, res, next){
User.findById(req.session.userId, function (err, user) {
// todo: don't forget to handle err
if (!user) {
return res.redirect('/edit');
}
// good idea to trim
var bio = req.body.bio.trim();
// validate
if (!bio) { // simplified: '' is a falsey
req.flash('error', 'One or more fields are empty');
return res.redirect('/settings-user'); // modified
}
// no need for else since you are returning early ^
user.bio = bio;
// don't forget to save!
user.save(function (err) {
// todo: don't forget to handle err
res.redirect('/settings-user/');
});
});
});
The User model :
var mongoose = require('mongoose');
var bcrypt = require('bcrypt');
var UserSchema = new mongoose.Schema({
email: {
type: String,
unique: true,
required: true,
trim: true
},
name: {
type: String,
required: true,
trim: true
},
password: {
type: String,
required: true
},
bio: {
type: String
}
});
// authenticate input against database documents
UserSchema.statics.authenticate = function(email, password, callback) {
User.findOne({ email: email })
.exec(function (error, user) {
if (error) {
return callback(error);
} else if ( !user ) {
var err = new Error('User not found.');
err.status = 401;
return callback(err);
}
bcrypt.compare(password, user.password , function(error, result) {
if (result === true) {
return callback(null, user);
} else {
return callback();
}
})
});
}
// hash password before saving to database
UserSchema.pre('save', function(next) {
var user = this;
bcrypt.hash(user.password, 10, function(err, hash) {
if (err) {
return next(err);
}
user.password = hash;
next();
})
});
var User = mongoose.model('User', UserSchema);
module.exports = User;
the pug file :
div
form(method='post', action='/settings-user')
label ADD BIO
br
input(type='text', name='bio', placeholder='Enter something', required='')
input(type='submit', value='Add Bio')
</body>
If anyone could help,,,
thank you!
im building a website in my free time using nodejs/Handlebars.js/mongodb(mongoose),
i didnt study any web developement lessons, and i dont know the right way to do things efficiently and securely.
so in this project, i stumbled upon a problem where i had to query in the database if email already exists or no if it does, it queries again if the username already exists or not, if it does exists the user can be registred to the databse. yes it gets the job done but im not satisfyied with this approach, it seems to be unprofessional and not secure.
so can you please tell me the right way to do it ?
this is the part where i think i ve done it the wrong way
//check for errors in Req.validation and push them to errors Array
if(valErrors){
for (var i = 0; i < valErrors.length; i++) {
errors.push(valErrors[i])
}
}
//check if the username submitted exists in the database
User.findOne({'username':username}, function (err, user) {
if(user)
{
errors.push({msg:"username is already in use!"})
res.render('user/register',{
errors:errors
});
}
//if the username is not in use already check if the email is in
//use
else {
User.findOne({'email':email}, function (err, user) {
if(user){
errors.push({msg:'email is already in use !'})
res.render('user/register',{
errors:errors
});
} //if the email doesnt exists too then register this //user
else{
var coins = new Coins()
var newUser = new User({
name: name,
email:email,
username: username,
password: password,
coins:coins.encryptcoins('0'),
joindate:getDate()
});
User.createUser(newUser, function(err, user){
if(err) throw err;
});
req.flash('success_msg', 'You are registered and can now login');
res.redirect('/user/login');
}
});
}
});
})
EDIT:
user Schema
var mongoose = require('mongoose');
var bcrypt = require('bcryptjs');
// User Schema
var UserSchema = mongoose.Schema({
username: {
type: String,
index:true,
required:true
},
password: {
type: String,
required:true
},
email: {
type: String,
required:true
},
name: {
type: String,
required:true
},
coins: {
type:String,
required:true
},
joindate: {
type:String,
required:true
},
orders: {
type:Array,
required:false
}
},{collection:'Users'});
var User = module.exports = mongoose.model('User', UserSchema);
module.exports.createUser = function(newUser, callback){
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(newUser.password, salt, function(err, hash) {
newUser.password = hash;
newUser.save(callback);
});
});
}
module.exports.getUserByUsername = function(username, callback){
var query = {username: username};
User.findOne(query, callback);
}
module.exports.getUserById = function(id, callback){
User.findById(id, callback);
}
module.exports.comparePassword = function(candidatePassword, hash, callback){
bcrypt.compare(candidatePassword, hash, function(err, isMatch) {
if(err) throw err;
callback(null, isMatch);
});
}
this is the whole code
var express = require('express');
var router = express.Router();
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var User = require('../models/users');
const ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn();
const ensureLoggedOut = require('connect-ensure-login').ensureLoggedOut();
var Coins = require('../models/coins');
// Register
router.get('/register',ensureLoggedOut, function(req, res){
res.render('user/register');
});
// Login
router.get('/login',ensureLoggedOut, function(req, res){
res.render('user/login');
});
// Register User
router.post('/register', function(req, res){
var name = req.body.name;
var email = req.body.email;
var username = req.body.username;
var password = req.body.password;
var password2 = req.body.password2;
console.log(email)
console.log(username)
// Validation
req.checkBody('name', 'Name is required').notEmpty();
req.checkBody('email', 'Email is required').notEmpty();
req.checkBody('email', 'Email is not valid').isEmail();
req.checkBody('username', 'Username is required').notEmpty();
req.checkBody('password', 'Password is required').notEmpty();
req.checkBody('password2', 'Passwords do not match').equals(req.body.password);
//Error handling
var errors = [];
var valErrors = req.validationErrors()
//check for errors in Req.validation and push them to errors Array
if(valErrors){
for (var i = 0; i < valErrors.length; i++) {
errors.push(valErrors[i])
}
}
//check if the username submitted exists in the database
User.findOne({'username':username}, function (err, user) {
if(user)
{
errors.push({msg:"username is already in use!"})
res.render('user/register',{
errors:errors
});
}
//if the username is not in use already check if the email is in
//use
else {
User.findOne({'email':email}, function (err, user) {
if(user){
errors.push({msg:'email is already in use !'})
res.render('user/register',{
errors:errors
});
} //if the email doesnt exists too then register this //user
else{
var coins = new Coins()
var newUser = new User({
name: name,
email:email,
username: username,
password: password,
coins:coins.encryptcoins('0'),
joindate:getDate()
});
User.createUser(newUser, function(err, user){
if(err) throw err;
});
req.flash('success_msg', 'You are registered and can now login');
res.redirect('/user/login');
}
});
}
});
})
passport.use(new LocalStrategy(
function(username, password, done) {
User.getUserByUsername(username, function(err, user){
if(err) throw err;
if(!user){
return done(null, false, {message: 'Unknown User'});
}
User.comparePassword(password, user.password, function(err, isMatch){
if(err) throw err;
if(isMatch){
return done(null, user);
} else {
return done(null, false, {message: 'Invalid password'});
}
});
});
}));
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.getUserById(id, function(err, user) {
done(err, user);
});
});
router.post('/login',
passport.authenticate('local', {successReturnToOrRedirect: '/', failureRedirect:'/user/login',failureFlash: true}),
function(req, res) {
res.redirect('/');
});
router.get('/logout',ensureLoggedIn, function(req, res){
req.logout();
req.session.destroy();
res.redirect('/');
});
module.exports = router;
function getDate(){
var d = new Date()
return ("date: "+d.getDate()+"/"+(d.getMonth()+1)+"/" +d.getFullYear() + " time GMT+1: "+(d.getHours()+1)+":"+(d.getMinutes())).toString()
}
// replaced with Ensure loging in library !
// function ensureLoggedIn(req, res, next) {
// if(req.user){
// return next()
// }else{
// res.redirect('/user/login');
// }
// }
// function ensureLoggedOut(req, res, next) {
// if(!req.user){
// return next()
// }else{
// res.redirect('/');
// }
// }
In general, for a logical unit of work send to a Database Management System (DBMS) (i.e., MongoDB server), it is imperative to group the individual operations in a single transaction. This way, you can avoid inconsistencies that might result from concurrent user creation in your database.
To be more precise, in your project the registration process checks for the following:
Check if email exists
Check if username exists
If queries 1 and 2 returned an empty result set, register a new user
In essence, those 3 steps need to take place in an atomic fashion, which means that they occur as a single logical unit (Transaction). If not, in the extreme case that 2 concurrent clients try to register users with the same usernames, then your database will result with two users with the same username.
Therefore, you should update your code to do the following:
Initiate a transaction
Check for users with the given email (user_email) and or usernamae (user_name)
If the query of step 2 returned a user, then rollback the transaction; Otherwise, insert a new user with user_email and user_name.
Commit Transaction
I am not sure whether MongoDB supports Transactional Consistency, and this is one of the reasons that I suggested using an RDBMS. Also, if it doesn't, I am sure that you can figure out a schema that identifies a single user based on email/username and try to perform the registration as a transaction.
Finally, it is considered good practice to have most of the processing take place in the DBMS side with the use of Stored Procedures.
I hope this helps.
I am using Mongo in my app, I have a db named test, what I am doing now is just an app to understand the workflow among Angular, Nodejs and Mongo.
The issue I have right now, or actually I don't know if this an issue but, I created an user with the name of User1 and a given email address, then I went to the app and saved some stuff, I logged out and then logged in again to confirm that the info I saved was there, and YES! the info was there it doesn't matter how many times and logged in and logged out.
Then I created another user with the name of User2 and obviously a different email address, but when I logged in, the information I had save from User1 was there.
I am on a local environment:
Node version: 5.0.0
MongoDB shell version: 3.2.0
this is what I have regarding the user schema
var userSchema = new mongoose.Schema({
name: { type: String, trim: true, required: true },
email: { type: String, unique: true, lowercase: true, trim: true },
password: String,
facebook: {
id: String,
email: String
},
google: {
id: String,
email: String
}
});
userSchema.pre('save', function(next) {
var user = this;
if (!user.isModified('password')) return next();
bcrypt.genSalt(10, function(err, salt) {
if (err) return next(err);
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) return next(err);
user.password = hash;
next();
});
});
});
userSchema.methods.comparePassword = function(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
});
};
var User = mongoose.model('User', userSchema);
mongoose.connect('localhost');
and here in Nodejs the signup route
app.post('/auth/signup', function(req, res, next) {
var user = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
user.save(function(err) {
if (err) return next(err);
res.sendStatus(200);
});
});
and the service un Angular
signup: function(user) {
return $http.post('/auth/signup', user)
.success(function() {
$location.path('/login');
});
}
the signup controller
.controller('SignupCtrl', function($scope, Auth) {
$scope.signup = function() {
Auth.signup({
name: $scope.displayName,
email: $scope.email,
password: $scope.password
});
};
Here is the logging part:
Node:
app.post('/auth/login', function(req, res, next) {
User.findOne({ email: req.body.email }, function(err, user) {
if (!user) return res.status(401).send('User does not exist');
user.comparePassword(req.body.password, function(err, isMatch) {
if (!isMatch) return res.status(401).send('Invalid email and/or password');
var token = createJwtToken(user);
res.send({ token: token });
});
});
});
Angular Service
login: function(user) {
return $http.post('/auth/login', user).success(function(data) {
$window.localStorage.token = data.token;
var payload = JSON.parse($window.atob(data.token.split('.')[1]));
$rootScope.currentUser = payload.user;
$location.path('/');
})
}
Controller
.controller('LoginCtrl', function($scope, Auth) {
$scope.login = function() {
Auth.login({ email: $scope.email, password: $scope.password });
};
Update
var tokenSecret = 'whatever goes here';
function ensureAuthenticated(req, res, next) {
if (req.headers.authorization) {
var token = req.headers.authorization.split(' ')[1];
try {
var decoded = jwt.decode(token, tokenSecret);
if (decoded.exp <= Date.now()) {
res.status(400).send('Access token has expired');
} else {
req.user = decoded.user;
return next();
}
} catch (err) {
return res.status(500).send('Error parsing token');
}
} else {
return res.sendStatus(401);
}
}
function createJwtToken(user) {
var payload = {
user: user,
iat: new Date().getTime(),
exp: moment().add(7, 'days').valueOf()
};
return jwt.encode(payload, tokenSecret);
}
I created this Gist with the full code
Here is the Github repository with all of the info
what do you think its happening?
I am trying to encrypt user passwords using Bcrpyt for my Angular app which uses Mongodb in the backend.
Here is the code
Model
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
bcrypt = require('bcryptjs'),
SALT_WORK_FACTOR = 10;
var UserSchema = new mongoose.Schema({
name: String,
username: { type: String, required: true, index: { unique: true } },
email: String,
password: { type: String, required: true },
created_at: Date,
topics: [{type: Schema.Types.ObjectId, ref: 'Topic'}],
posts: [{type: Schema.Types.ObjectId, ref: 'Post'}],
comments: [{type: Schema.Types.ObjectId, ref: 'Comment'}]
});
UserSchema.pre('save', function(next) {
var user = this;
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) return next();
// generate a salt
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
if (err) return next(err);
// hash the password along with our new salt
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) return next(err);
// override the cleartext password with the hashed one
user.password = hash;
next();
});
});
});
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
if (err) return cb(err);
cb(null, isMatch);
});
};
mongoose.model('User', UserSchema);
Create & Login inside Controller
var mongoose = require('mongoose');
var User = mongoose.model('User');
module.exports = (function() {
return {
login: function(req, res) {
User.findOne({email: req.body.email}, function(err, user) {
if(user === null) {
var error = "User not found"
console.log(error);
}
else{
user.comparePassword(req.body.password, function(err, isMatch){
if(err){
console.log("Password dont match");
} else{
console.log(user)
res.json(user);
}
})
}
})
},
create: function(req, res) {
var user = new User({name: req.body.name, username:req.body.username, email:req.body.email, password:req.body.password, created_at: req.body.created_at});
user.save(function(err) {
if(err) {
console.log('something went wrong');
} else {
console.log('successfully added a user!');
res.redirect('/');
}
})
}
})();
The user create function is working fine, saving in the passwords encrypted. But during Login it is not properly comparing the encrypted password against the input. Lets user through regardless of any password.
Also how would I go about showing errors incase of user not found and also for password not matching(this is a secondary Q.
Primary concerned about even wrong password being accepted.
Thanks for the Help.
You are checking if there is any error during password matching but not checking if the input password matches the hashed one.
user.comparePassword(req.body.password, function(err, isMatch){
if(err){
return console.log("Password dont match");
}
if (isMatch) {
// password matches. Log the user in
}
});
I'm trying to make a login in for my app using mongoose, passport-local, and bcrypt-nodejs.
The userSchema pre('save') function works fine and saves a hashed password. however the bcrypt compare method will return false every time.
see bcrypt-nodejs
here is my userSchema
var userSchema = mongoose.Schema({
login:{
local:{
email: {type: String, unique: true, required: true},
password: {type: String, unique: true, required: true}
}
}
userSchema.pre('save', function(next) {
bcrypt.hash('user.login.local.password', null, null, function(err, hash){
if(err){
next(err);
}
console.log('hash', hash);
user.login.local.password = hash;
next();
})
});
userSchema.methods.validPassword = function(password, cb){
bcrypt.compare(password, this.login.local.password, function(err, isMatch){
if(err) return cb(err);
cb(null, isMatch);
})
module.exports = mongoose.model('User', userSchema);
this works fine, and saves a new user with a hashed password
here is my my login strategy
no matter what info the user inputs, this will always return false
passport.use('local-login', new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallBack: true
},
function(email, password, done){
User.findOne({ 'login.local.email' : email }, function(err, user){
if(err){
console.log(err);
return done(err);
}
if(!user){
console.log('no user found');
return done(err);
}
user.validPassword(password, function(err,match){
if(err){
console.log(err);
throw err;
}
console.log(password, match);
})
})
}))
lastly my route
app.post('/user/login', passport.authenticate('local-login'{
successRedirect: '/#/anywhereBUThere'
failureRedirect: '/#/'
}))
Most likely the root of the problem is that the compare function is returning false because you are indeed comparing two non-identical hashes.
You appear to be passing in a string 'user.login.local.password' instead of the actual password in your userSchema pre save function:
e.g. this
bcrypt.hash('user.login.local.password', null, null, function(err, hash){ should be bcrypt.hash(user.login.local.password, null, null, function(err, hash){ (no single-quotes on the password being passed in as the first parameter.)
Additionally, you're then setting the generated hash to a 'user' object which seems to live outside of your user model. I can't see that code, but I suspect that you're not updating the value of the hash on the user model being saved to mongoDB.
e.g.
user.login.local.password = hash;
should probably be
this.login.local.password = hash;
I had a similar issue where bcrypt.compare() is always returning false, turns out I was passing the arguments in an incorrect order, please make sure to pass the plain password as the first argument.
bcrypt.compare(plainPassword, hashedPassword)