I'm developing an app using Node.js, Mongoose, MongoDb, Express.
I'm trying to hash the password before being saved in the db when the user register, but it doesn't seems to work. The password is saved without hashing, any suggestions?
'use strict'
let mongoose = require('mongoose')
let bcrypt = require('bcrypt-nodejs')
var Schema = mongoose.Schema
var userSchema = Schema({
name: { type: String, required: true, unique: true },
password: { type: String, required: true },
createdAt: {
type: Date,
require: true,
default: Date.now
}
})
// check if user already exists
userSchema.path('name').validate(function (name) {
User.findOne({name}, function (err, user) {
if (err) {
console.log('error')
} if (user) {
console.log('The user already exists')
console.log(user)
}
})
}, 'The user already exists')
// password validation
userSchema.path('password').validate(function (password) {
return password.length >= 6
}, 'The password must be of minimum length 6 characters.')
var User = mongoose.model('User', userSchema)
// hashing and adding salt pre save()
userSchema.pre('save', function (next) {
bcrypt.genSalt(10, function (err, salt) {
if (err) {
return next(err)
}
bcrypt.hash(this.password, salt, null, function (err, hash) {
// Store hash in your password DB.
if (err) {
return next(err)
}
// set the password to the hash
this.password = hash
})
next()
})
})
module.exports = User
Its because you do next() before bcrypt.hash callback is called. Move next() into bcrypt.hash callback.
userSchema.pre('save', function(next) {
bcrypt.genSalt(10, function(err, salt) {
if (err) {
return next(err)
}
bcrypt.hash(this.password, salt, null, function(err, hash) {
// Store hash in your password DB.
if (err) {
return next(err)
}
// set the password to the hash
this.password = hash
next()
})
})
})
next() should be called within bcrypt.hash() method when using callbacks.
For synchronous:
userSchema.pre('save', (next) => {
const salt = bcrypt.genSaltSync(10)
const hash = bcrypt.hashSync(this.password, salt)
this.password = hash
next()
})
Related
I am developping a web app with the MERN stack.
On Postman or on my front-end form, when I register a user with an existing email, the user is not added to database. I use the same logic to check if the username picked by the user is already taken. If it's taken, the error message is dispatched but the user is still added to the database.
In the User model, the field Username is unique, just like the field Email.
My register route:
const express = require("express");
const router = express.Router();
const User = require("../../models/User");
const { check, validationResult } = require("express-validator");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const config = require("config");
// #route GET register
// #desc Register
// #access Public
router.get("/", (req, res) => {
res.send("Create an account");
});
// #route POST register
// #desc Register
// #access Public
router.post(
"/",
[
check("email", "Please, include a valid email.").isEmail(),
check(
"password",
"Please, enter a password with 6 or more characters"
).isLength({ min: 6 }),
check("username", "Please, include a valid username.")
.not()
.isEmpty()
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
email,
password,
birthdate,
birthplace,
sun,
username,
date,
is_paying
} = req.body;
try {
// See if user exists
let user = await User.findOne({ $or: [{ username }, { email }] });
if (user) {
res.status(400).json({ errors: [{ msg: "User already exists" }] });
}
// Create new user from User Model
user = new User({
email,
password,
birthdate,
birthplace,
sun,
username,
date,
is_paying
});
// Encrypt password
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// Add user to database
await user.save();
// Return JWT
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
config.get("jwtSecret"),
{ expiresIn: 360000 },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send("Server Error");
}
}
);
module.exports = router;
My register action :
// REGISTER USER
export const register = ({
email,
password,
birthdate,
gender,
sun,
username
}) => async dispatch => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
const body = JSON.stringify({
email,
password,
birthdate,
gender,
sun,
username
});
try {
const res = await axios.post("/register", body, config);
dispatch({
type: REGISTER_SUCCESS,
payload: res.data
});
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach(error => dispatch(setAlert(error.msg, "danger")));
}
dispatch({
type: REGISTER_FAIL
});
}
};
You need to stop processing the request if the user exists:
// See if user exists
let user = await User.findOne({ $or: [{ username }, { email }] });
if (user) {
res.status(400).json({ errors: [{ msg: "User already exists" }] });
}
// Create new user from User Model
user = new User({
email,
password,
birthdate,
birthplace,
sun,
username,
date,
is_paying
});
See that? if (user) { res.json() } but you still go on. Make that return res.json() for a quick fix.
A bit better fix is you should set a unique index on the username and on the email fields. In mongoose it looks something like:
const userSchema = new Schema({
name: {
type: String,
index: {
unique: true
}
}
});
Or if you want to allow same usernames but the uniqueness to be the username-email combo, make a compound index.
And even better - move the whole thing to another file. Call the file user-registration.service.js. Make the file export one function:
async function registerUser(username, email, password) {
// check uniqueness here. if it fails, _throw an error!_
// if all is ok, save the user, return the details.
}
module.exports = {
registerUser
}
That way your route controller can just say:
router.post(
"/",
[
check("email", "Please, include a valid email.").isEmail(),
check(
"password",
"Please, enter a password with 6 or more characters"
).isLength({ min: 6 }),
check("username", "Please, include a valid username.")
.not()
.isEmpty()
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
email,
password,
birthdate,
birthplace,
sun,
username,
date,
is_paying
} = req.body;
try {
const user = await registerUser();
return res.json(user); // or token or whatnot.
} catch (err) {
// see here? only one place to deal with that error.
return res.status(400)
.json(err);
}
}
Now you only have that one place to deal with errors. You could even push it further and simply omit that try/catch and let a global error handler deal with it.
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
email,
password,
birthdate,
birthplace,
sun,
username,
date,
is_paying
} = req.body;
try {
// See if user exists
let user = await User.findOne({ $or: [{ username }, { email }] });
if (user) {
res.status(400).json({ errors: [{ msg: "User already exists" }] });
}
else{
// Create new user from User Model
user = new User({
email,
password,
birthdate,
birthplace,
sun,
username,
date,
is_paying
});
// Encrypt password
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// Add user to database
await user.save();
// Return JWT
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
config.get("jwtSecret"),
{ expiresIn: 360000 },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
}
}
catch (err) {
console.error(err.message);
res.status(500).send("Server Error");
}
}
);
Add the add new user code in else section
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!
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();
})
})
});
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
}
});