I'm working on my password reset flow and everything works except the actual bcrypt password reset. Following the "Technique 2 (auto-gen a salt and hash)" from about halfway down in the bcrypt docs suggests following below syntax:
Bcrypt docs syntax
const hash = bcrypt.hashSync(myPlaintextPassword, saltRounds);
// Store hash in your password DB.
My function
module.exports.submitNewPassword = async (req, res) => {
// console.log(req.body.password, req.params, req.headers.referer );
const slidedHeaderToken = req.headers.referer.slice(-40);
const user = await User.findOne({ resetPasswordToken: slidedHeaderToken, resetPasswordExpires: { $gt: Date.now() } });
console.log("submitNewPassword user ", user);
if (!user) {
console.log("user not found");
req.flash('error', "Password reset token is invalid or has expired");
res.render('users/reset')
} else {
// hash the new password
const hashedPassword = await bcrypt.hash(req.body.password, 12);
// update the user's password
user.password = hashedPassword;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
await user.save();
res.redirect('/users/login');
}
}
The resetPasswordToken and resetPasswordExpires are set properly in my previous function which emails out the password reset link and saves the two variables to the database.
The if statement logic and user.resetPasswordToken = undefined; and user.resetPasswordExpires = undefined; are executed properly but I can't get user.password = hashedPassword; to work.
How do I reset the user's password?
Related
I'm implementing a "forgot password" system in my authentication API in node. For this I created a "forgetPassword" controller where I send the user's email through req.body and with that an email with the recovery token is sent to the user, and that same token is also sent inside of the model user in the database in "tokenReset". The second controller is "resetPassword", where this recovery token is sent by params and a new password is sent in the req.body.
The biggest problem is, I think it's not safe for this token to be in the database without being encrypted, as someone inside the database can take advantage of this as a flaw, so I encrypted the token before sending it to the database. But the big question is, I have to fetch this token inside the database in "resetPassword" controller through the token sent in the params, but the token inside my database is encrypted and the one in the params is not. What would be the best way to resolve this?
forgotPassword controller
const jwt = require('jsonwebtoken');
const db = require('../../models/index');
const sendEmail = require('../../utils/mailer');
const bcrypt = require('bcryptjs');
exports.store = async (req, res) => {
const { email } = req.body;
const secret = process.env.JWT_SECRET;
try {
const user = await db.User.findOne({ where: { email } });
if (!user) {
res.status(400).json('User does not exist.');
}
const token = jwt.sign({ id: user.id }, secret, {
expiresIn: process.env.EXPIRES_FORGOTPASS,
});
const hashedToken = bcrypt.hashSync(token, 10);
await user.update({
tokenReset: hashedToken,
});
sendEmail({
from: 'noreply#email.com',
to: 'admin#gmail.com',
subject: 'Reset your password',
text: `Token sending email to reset password account from ${user.email}`,
html: `<h2>Token sending email to reset password account from ${user.email}</h2>
<h4>${token}</h4>
`,
});
return res.status(200).json('Check the verification link in your email!');
} catch (err) {
return console.log(err);
}
}
resetPassword controller
const jwt = require('jsonwebtoken');
const db = require('../../models/index');
const bcrypt = require('bcryptjs');
const sendEmail = require('../../utils/mailer');
exports.store = async (req, res) => {
const { token } = req.params;
const { password } = req.body;
const secret = process.env.JWT_SECRET;
try {
const userExists = await db.User.findOne({ where: { tokenReset: token } });
if (!userExists) {
return res.status(401).json('User not found in our database!');
}
try {
await jwt.verify(token, secret);
} catch (err) {
return res.status(401).json('Link expired, make a new password reset request!');
}
const hashedNewPass = bcrypt.hashSync(password, 10);
await userExists.update({
password: hashedNewPass,
tokenReset: "",
});
return res.status(200).json(`Password updated!`);
} catch (err) {
return console.log(err);
}
}
This is the code for login authorization
exports.login = catchAsync(async(req, res, next) => {
// const { email, password } = req.body;
const email = req.body.email;
const password = req.body.password;
// 1. check if the email and password exist
if (!email || !password) {
return next(new AppError('please provide password or email!', 400));
}
// 2. check if the email and password are corrects
const user = await User.findOne({ email }).select('+password');
const correct = await user.correctPassword(password,user.password);
if (!user || !correct) {
return next(new AppError('please enter valid email or password', 401));
}
// 3. if all ok, send token to client
const token = '';
res.status(200).json({
status: 'sucess',
token,
user,
});
});
the code is looking correct!
assigning null to the user after await findone query
I don't know why this hash password code does not work.
I did install bcrypt, also, it should go to the line (res.send("testing"))if the passwords are the same but anyway in all situations password does not match is coming even they are the same.
Here is my code:
const mysql = require('mysql');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const db = mysql.createConnection({
host: process.env.DATABASE_host,
user: process.env.DATABASE_user,
password: process.env.DATABASE_password,
database: process.env.DATABASE,
});
exports.form = (req, res) => {
console.log(req.body);
const { name, email, password, confirmPassword } = req.body;
db.query(
'SELECT email FROM users WHERE email=?',
[email],
async (error, results) => {
if (error) {
console.log(error);
}
if (results.length > 0) {
return res.render('form', {
message: 'that email is already in use',
});
} else if (password !== confirmPassword) {
return res.render('form', {
message: 'passwords not match',
});
}
let hashedPassword = await bcrypt.hash('password', 8);
console.log(hashedPassword);
res.send('testing');
}
);
};
``
[enter image description here][1]
[1]: https://i.stack.imgur.com/ToNvN.png
and always (passwords not match) comes even as u see in pic the passwords are same
Every time you call bcrypt.hash() you will get a different hash string, even with the same password, this is because the hashes are salted.
To check whether the hashes are equal, you need to test with bcrypt.compare(), you cannot compare to hashes directly. Some libraries also call it bcrypt.verify().
Edit: Assuming you use the node.bcrypt.js library:
const bcrypt = require('bcrypt');
// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
let hashToStoreInDb = bcrypt.hashSync('mypassword', 10);
// Check if the entered login password matches the stored hash.
// The salt and the cost factor will be extracted from existingHashFromDb.
let existingHashFromDb = hashToStoreInDb;
const isPasswordCorrect = bcrypt.compareSync('mypassword', existingHashFromDb);
It's me again asking for help in Nodejs. I tried processing the resetPassword function that I created on Nodejs using Postman but I kept having the same error. I also noticed from my MongoDB Compass that the resetPasswordToken on the database and the resetPasswordToken sent using mailtrap is not the same. Here are my codes:
//user.js
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const crypto = require('crypto')
const userSchema = new mongoose.Schema({
name:{
type: String,
required: [true, 'Please enter your name'],
maxLength: [30, 'Your name cannot exceed 30 characters']
},
email:{
type: String,
required: [true, ' Please enter your email'],
unique: true,
validate:[validator.isEmail, 'Please enter a valid email address.']
},
password:{
type:String,
required: [true, 'Please enter your password'],
minlength: [6, 'Your password must be longer that 6 characters.'],
select: false
},
role:{
type: String,
default: 'admin'
},
createdAt: {
type: Date,
default:Date.now
},
resetPasswordToken: String,
resetPasswordExpire: Date
})
//Encrypting password before saving user
userSchema.pre('save',async function(next){
if(!this.isModified('password')){
next()
}
this.password = await bcrypt.hash(this.password, 10)
})
//Compare user password
userSchema.methods.comparePassword = async function(enteredPassword){
return await bcrypt.compare(enteredPassword, this.password)
}
//Return JWT token
userSchema.methods.getJwtToken = function(){
return jwt.sign({id:this._id}, process.env.JWT_SECRET,{
expiresIn: process.env.JWT_EXPIRES_TIME
})
}
// Generate password reset token
userSchema.methods.getResetPasswordToken = function(){
//Generate token
const resetToken = crypto.randomBytes(20).toString('hex');
//Hash and set to resetPasswordToken
this.resetPasswordToken = crypto.createHash('sha256').update(resetToken).digest('hex')
//Set token expire time
this.resetpasswordExpire = Date.now() + 30 * 60 * 1000
return resetToken
}
module.exports = mongoose.model('User', userSchema);
//authController.js
const User = require('../models/user')
const ErrorHandler = require('../utils/errorHandler');
const catchAsyncErrors = require('../middlewares/catchAsynchErrors');
const sendToken = require('../utils/jwtToken');
const sendEmail = require('../utils/sendEmail')
const crypto = require('crypto')
//Register a user => /api/v1/register
exports.registerUser = catchAsyncErrors (async (req, res, next) =>{
const { name, email, password } =req.body;
const user = await User.create({
name,
email,
password
})
sendToken(user, 200, res)
})
//Login User => api/v1/login
exports.loginUser = catchAsyncErrors (async (req,res,next) =>{
const { email, password} = req.body;
//Checks if email and password is entered by user
if(!email || !password){
return next(new ErrorHandler('Please enter email and password', 400))
}
//Finding the user in database
const user = await User.findOne({email}).select('+password')
if(!user){
return next(new ErrorHandler('Invalid Email or Password', 401));
}
//Checks if password or correct or not
const isPasswordMatched = await user.comparePassword(password)
if (!isPasswordMatched) {
return next(new ErrorHandler('Invalid Email or Password', 401));
}
sendToken(user,200,res)
})
//Forgot Password => api/v1/password/forgot
exports.forgotPassword = catchAsyncErrors(async(req, res, next) => {
const user = await User.findOne({email: req.body.email});
if(!user){
return next(new ErrorHandler('User not found', 404));
}
//Get reset token
const resetToken = user.getResetPasswordToken();
await user.save({validateBeforeSave: false })
//Create reset password url
const resetUrl =`${req.protocol}://${req.get('host')}/api/v1/password/reset/${resetToken}`;
const message = `Your password reset token is as follows:\n\n${resetUrl}\n\n If you have not requested this email, then please ignore.`
try{
await sendEmail({
email: user.email,
subject: "KPOPStore Password Recovery",
message
})
res.status(200).json({
success: true,
message: `Email sent to ${user.email}`
})
}catch (error){
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
await user.save({validateBeforeSave: false })
return next(new ErrorHandler(error.message, 500))
}
})
//ResetPassword => /api/v1/password/reset/:token
exports.resetPassword = catchAsyncErrors(async(req, res, next) =>{
//Hash URL Token
const resetPasswordToken = crypto.createHash('sha256').update(req.params.token).digest('hex')
const user = await User.findOne({
resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() }
})
if(!user){
return next(new ErrorHandler('Password reset token is invalid or has been expired.', 400)
)
}
if(req.body.password !== req.body.confirmPassword){
return next(new ErrorHandler('Password does not match', 400))
}
//Setup new password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
await user.save();
sendToken(user, 200, res)
})
//Logout user => /api/v1/logout
exports.logout = catchAsyncErrors(async (req,res,next)=>{
res.cookie('token', null, {
expires: new Date(Date.now()),
httpOnly: true
})
res.status(200).json({
success: true,
message: 'Logged out'
})
})
//jwtToken.js
//Create and send token and save in cookie.
const sendToken =( user, statusCode, res)=>{
//Create Jwt token
const token = user.getJwtToken();
//Options for cookie
const options = {
expires: new Date(
Date.now() + process.env.COOKIE_EXPIRES_TIME * 24 * 60 * 60 * 1000
),
httpOnly: true
}
res.status(statusCode).cookie('token', token, options).json({
success: true,
token,
user
})
}
module.exports = sendToken;
//auth.js
const express = require('express');
const router = express.Router();
const { registerUser, loginUser, logout, forgotPassword, resetPassword} = require('../controllers/authController')
router.route('/register').post(registerUser);
router.route('/login').post(loginUser);
router.route('/password/forgot').post(forgotPassword)
router.route('/password/reset/:token').put(resetPassword)
router.route('/logout').get(logout);
module.exports = router;
I'm sorry for the long blocks of code. I've been stuck in this part for 3 days. Please help me again. Thank you!
It's a typo in the User model's method:
//Set token expire time
this.resetpasswordExpire = Date.now() + 30 * 60 * 1000
It sets resetpasswordExpire, not resetPasswordExpire, so the change is not picked up by the Object-Document Mapper and not saved in the DB. Then, your search fails:
User.findOne({
resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() }
})
because resetPasswordExpire is not set.
In a mean stack app I create a new user in User Schema then a new doc in Doc Schema
var UserSchema = new Schema({
username: String,
password: String,
docs: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Doc'
}],
)
}
var DocSchema = new Schema({…)
}
UserSchema.pre('save', function(next) {
if (this.password) {
this.salt = new Buffer(crypto.randomBytes(16).toString('base64'), 'base64');
this.password = this.hashPassword(this.password);
}
next();
});
The following parts of code is the passport signup where at the end I have an issue with newUser.save(); After the push method if I do not save the user, doc ID are not displayed in user document. But saving user seems to change the hashed password also. If I comment newUser.save(); login works fine otherwise I get a wrong password
passport.use('local-signup', new LocalStrategy({
usernameField: 'username',
passwordField: 'password',
passReqToCallback: true
},
function(req, username, password, done) {
process.nextTick(function() {
User.findOne({
username: username
}, function(err, user) {
// if there are any errors, return the error
if (err)
return done(err);
if (user) {
return done(null, false, req.flash('signupMessage', 'Username is already taken.'));
} else {
var newUser = new User();
newUser.username = username;
newUser.password = password;
newUser.email = req.body.email;
newUser.save(function(err) {
if (err)
throw err;
var doc = new Doc({
user: newUser.username,
docTitle: newUser.username
});
doc.save(function(err) { // create doc
if (err) {
return next(err);
} else {
newUser.docs.push(doc); // push doc'id in docs field in user
newUser.save(); // save user after doc'id has been push
};
return done(null, newUser);
});
});
Any help would be appreciated
Your logic in your mongoose pre save middleware says 'if there is a password on the document being saved, generate a salt and hash the password'. Therefore, if a password exists on the document that has already been salted and hashed, when the middleware runs it will salt and hash this pre-existing password again. This is why you can't login a second time; your password is changing every time you save the document.
I'm guessing you expect the mongoose pre save middleware to run only when you save the document the first time. The pre save middleware runs every time you save the document. There is a this.isNew property available on the document in the pre save middleware that you can make use of. This will ensure the password is only generated the first time you save the document.
UserSchema.pre('save', function(next) {
if (this.password && this.isNew) {
this.salt = new Buffer(crypto.randomBytes(16).toString('base64'), 'base64');
this.password = this.hashPassword(this.password);
}
next();
});