18

I have made a set up to update a user's password using NodeJS/Passport. I followed this great guide: http://sahatyalkabov.com/how-to-implement-password-reset-in-nodejs/.

99% of this is working. I had to modify it a bit to include some stripe functionality. I am afraid however I have a critical error somewhere and I cannot find it. The user is currently able to go all the way through the process of having an email sent to them, enter a new password, and be logged in. Another email follows saying their password was successfully updated. All perfect. However. For some reason. The new password is not being SAVED. the user can only sign in with their old password. I have tried everything I can think of to fix this.

I have had several other programmers look at this and none of them have been able to figure out how in the world its not working.

Current thought is that the session might not be ending correctly but we tried destroying the session and it still did not work.

Any help greatly appreciated.

Full set up:

User Model:

var UserSchema = new mongoose.Schema({
    username:   { type: String, required: true, unique: true },
    password:  String,
    datapoint:  String,
    email:  { type: String, required: true, unique: true },
    resetPasswordToken: String,
    resetPasswordExpires: Date
});


UserSchema.pre('save', function(next) {
  var user = this;
  var SALT_FACTOR = 5;

  if (!user.isModified('password')) return next();

  bcrypt.genSalt(SALT_FACTOR, 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();
    });
  });
});

Register New Account (This also has stripe info in it unrelated but could cause the problem.)

var newUser = new User({username: req.body.username, email: req.body.email, datapoint: req.body.datapoint});
      User.register(newUser, req.body.password, function(err, user){


            if(err){
            console.log('Looks like there was an error:' + ' ' + err)
             res.redirect('/login')

          } else {
          passport.authenticate("local")(req, res, function(){

      var user = new User({
      username: req.body.username,
      email: req.body.email,
      password: req.body.password

      })


console.log('creating new account')
console.log('prepping charge')
var token = req.body.stripeToken; // Using Express
var charge = stripe.charges.create({
  amount: 749,
  currency: "usd",
  description: "Example charge",
  source: token,

}, function(err, charge) {
  // asynchronously called
  console.log('charged')
});
            res.redirect('/jobquiz')
             console.log(req.body.datapoint)
             console.log(req.body.email)

          });
          }
      });
});

Set up forgot password posting

app.post('/forgot', function(req, res, next) {
  async.waterfall([
    function(done) {
      crypto.randomBytes(20, function(err, buf) {
        var token = buf.toString('hex');
        done(err, token);
      });
    },
    function(token, done) {
      User.findOne({ email: req.body.email }, function(err, user) {
        if (!user) {
        //   console.log('error', 'No account with that email address exists.');
        req.flash('error', 'No account with that email address exists.');
          return res.redirect('/forgot');
        }
console.log('step 1')
        user.resetPasswordToken = token;
        user.resetPasswordExpires = Date.now() + 3600000; // 1 hour

        user.save(function(err) {
          done(err, token, user);
        });
      });
    },
    function(token, user, done) {
        console.log('step 2')


      var smtpTrans = nodemailer.createTransport({
         service: 'Gmail', 
         auth: {
          user: 'myemail',
          pass: 'mypassword'
        }
      });
      var mailOptions = {

        to: user.email,
        from: 'myemail',
        subject: 'Node.js Password Reset',
        text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
          'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
          'http://' + req.headers.host + '/reset/' + token + '\n\n' +
          'If you did not request this, please ignore this email and your password will remain unchanged.\n'

      };
      console.log('step 3')

        smtpTrans.sendMail(mailOptions, function(err) {
        req.flash('success', 'An e-mail has been sent to ' + user.email + ' with further instructions.');
        console.log('sent')
        res.redirect('/forgot');
});
}
  ], function(err) {
    console.log('this err' + ' ' + err)
    res.redirect('/');
  });
});

app.get('/forgot', function(req, res) {
  res.render('forgot', {
    User: req.user
  });
});

Set up changing password post

app.get('/reset/:token', function(req, res) {
  User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) {
      console.log(user);
    if (!user) {
      req.flash('error', 'Password reset token is invalid or has expired.');
      return res.redirect('/forgot');
    }
    res.render('reset', {
     User: req.user
    });
  });
});




app.post('/reset/:token', function(req, res) {
  async.waterfall([
    function(done) {
      User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user, next) {
        if (!user) {
          req.flash('error', 'Password reset token is invalid or has expired.');
          return res.redirect('back');
        }


        user.password = req.body.password;
        user.resetPasswordToken = undefined;
        user.resetPasswordExpires = undefined;
        console.log('password' + user.password  + 'and the user is' + user)

user.save(function(err) {
  if (err) {
      console.log('here')
       return res.redirect('back');
  } else { 
      console.log('here2')
    req.logIn(user, function(err) {
      done(err, user);
    });

  }
        });
      });
    },





    function(user, done) {
        // console.log('got this far 4')
      var smtpTrans = nodemailer.createTransport({
        service: 'Gmail',
        auth: {
          user: 'myemail',
          pass: 'mypass'
        }
      });
      var mailOptions = {
        to: user.email,
        from: 'myemail',
        subject: 'Your password has been changed',
        text: 'Hello,\n\n' +
          ' - This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
      };
      smtpTrans.sendMail(mailOptions, function(err) {
        // req.flash('success', 'Success! Your password has been changed.');
        done(err);
      });
    }
  ], function(err) {
    res.redirect('/');
  });
});
AndrewLeonardi
  • 3,351
  • 9
  • 47
  • 100
  • 1
    I tried you posted code without the stripe part and it is working fine for me. Its much harder to pinpoint issues when you have so many things happening. May be isolating calls and running them one at at time will help you more and I think it will help somebody answering if you can provide areas around you think it could be going wrong. – s7vr Mar 14 '17 at 00:39
  • I'm not sure if you have already found solution to your problem. There are couple of session setting that may be causing this issue which is missing in your config. I think its worth looking into if you havent already. Consider replacing `app.use(session({ secret: 'session secret key' }));` with `app.use(session({ secret: 'session secret key', resave: false, saveUninitialized: false })); app.use(passport.initialize()); app.use(passport.session());` and verify the order too. You can checkout more about these flags here https://github.com/expressjs/session#options – s7vr Mar 17 '17 at 02:06
  • Are all lines inside the pre-save middleware executed? That's where I'd begin debugging first. – GPX Mar 17 '17 at 11:45
  • It's generally not advised to tell the user that no user with the specified email address exists - even if that is the case. You should just say that "if a user with this email address exists, we have sent a password reset link." No reason to give a potential attacker more information. They way you have it, you're giving them a way to figure out exactly who your users are. – Jer Jul 11 '18 at 23:52

5 Answers5

14

I didn't (or haven't) find any problem with your code, but I have a suggestion to trace the bug.

This block of code is risky. You may accidentally update the password field and trigger the rehash password process.

UserSchema.pre('save', function(next) {
   var user = this;
   var SALT_FACTOR = 12; // 12 or more for better security

   if (!user.isModified('password')) return next();

   console.log(user.password) // Check accident password update

   bcrypt.genSalt(SALT_FACTOR, 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();
      });
   });
});

Put a console.log right after the if (!user.isModified('password')) to check for unexpected password update. Now retry forget the password and see if any bug in there.

*TD;LR Separate update password into a new method instead of putting it in the pre-save since you may accidentally update a new password along with other fields

*Update: Thanks #imns for suggesting a better SALT_FACTOR number.

Anh Cao
  • 482
  • 7
  • 13
  • 1
    Your salt factor should be at least 12. See: https://hackernoon.com/your-node-js-authentication-tutorial-is-wrong-f1a3bf831a46 – imns May 11 '18 at 19:56
5

I think the issue could be in the hash function. Tried duplicating you code into a simpler but similar experiment on my computer.

As the bcrypt docs state here https://www.npmjs.com/package/bcrypt#to-hash-a-password

The hash function only takes in 3 arguments, you send in 4. Whereas the third argument in your case is null.

Here is some code to illustrate the issue and the, hopefully, solution

Inside the salting callback

bcrypt.hash(user.password, salt, null, function(err, hash) {
  if (err) return next(err);
  user.password = hash;
  next();
});

But change the third argument to be the callback function instead.

bcrypt.hash(user.password, salt, function(err, hash) {
  if (err) return next(err);
  user.password = hash;
  next();
});
mertje
  • 486
  • 1
  • 7
  • 15
  • Thanks for giving it a shot! I agree that the problem must be somewhere in this step. I tried to implement the change you suggested and the error I get is: "No callback function was given." – AndrewLeonardi Mar 09 '17 at 17:35
  • Strange! Which version and package of bcrypt do you use, as stated in package.json. And if you create a new user, does the password get hashed at the first time? – mertje Mar 10 '17 at 04:29
  • 2
    @mertje OP is using `bcrypt-nodejs` not `bcrypt` – s7vr Mar 12 '17 at 22:17
1

I had the same issue just delete the null parameter from this line:

bcrypt.hash(user.password, salt, null, function(err, hash) {

1

If you do not want to implement the "forgot password" flow yourself - consider using authentication-flows-js. (available on npm of course)

It is a module that answers most flows - authentication, registration, forgot-password, change password etc., and it is secured enough so applications can use it without the fear that it will be easily hacked.

Read the full article with more explanations here.

OhadR
  • 8,276
  • 3
  • 47
  • 53
0

I already used this code in my current project, and its working fine, I saw a small error in your code in function UserSchema.pre('save', function(next). when you hash the password with bcrypt.hash then it took four arguments but there are only three argument in my code like

schema.pre('save', function(next) {
    var user = this;
    var SALT_FACTOR = 5;

    if(!user.isModified('password')){
        return next();
    }

    bcrypt.genSalt(SALT_FACTOR, 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();
        });
    });
});

Third argument must be callback function see document for bcrypt

Gaurav Kumar Singh
  • 1,550
  • 3
  • 11
  • 31
  • Author is not using `bcrypt`, but `bcrypt-nodejs`. For futher information, check out the article he referred to. – NikxDa Mar 18 '17 at 13:00