Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to reset / change password in Node.js with Passport.js?

Tags:

I use Passport.js in Node.js to create a login system. Everything is ok, but I do not know how to reset user password when they forget their password or they want to change it.

User model in MongoDB

var UserSchema = new Schema({     email: String,     username: String,     provider: String,     hashed_password: String,     salt: String, }); 
like image 363
user3044147 Avatar asked Nov 29 '13 01:11

user3044147


People also ask

How do I change my Passport password in Javascript?

You can reset or change passwords using 2 simple functions in passport-local-mongoose. They are setPassword function and changePassword functions. Normally setPassword is used when the user forgot the password and changePassword is used when the user wants to change the password.

What is Passport Localstrategy?

The local authentication strategy authenticates users using a username and password. The strategy requires a verify callback, which accepts these credentials and calls done providing a user.

What is Passport login in node js?

Passport is authentication middleware for Node. js. As it's extremely flexible and modular, Passport can be unobtrusively dropped into any Express-based web application. A comprehensive set of strategies supports authentication using a username and password, Facebook, Twitter, and more.


2 Answers

Didn't really like the idea of hitting my database to store tokens, especially when you want to be creating and verifying tokens for many actions.

Instead I decided to copy how Django does it:

  • convert timestamp_today to base36 as today
  • convert user.id to base36 as ident
  • create hash containing:
    • timestamp_today
    • user.id
    • user.last_login
    • user.password
    • user.email
  • salt the hash with a hidden secret
  • create a route like : /change-password/:ident/:today-:hash

We test the req.params.timestamp in order to simply test if it's valid for today, cheapest test first. fail first.

Then we find the user, fail if it doesn't exist.

Then we generate the hash again from above, but with the timestamp from req.params

The reset link becomes invalid if :

  • they remember their password and login (last_login changes)
  • they're actually still logged in and:
    • just change their password (password changes)
    • just change their email (email changes)
  • tomorrow arrives (timestamp changes too much)

This way:

  • you're not storing these ephemeral things in your database
  • when the purpose of the token is to change the state of a thing, and that things state changed, then the purpose of the token is no longer securely relevant.
like image 144
airtonix Avatar answered Oct 13 '22 06:10

airtonix


I tried to use node-password-reset as Matt617 suggested but didn't really care for it. It's about the only thing that's coming up in searches currently.

So some hours digging around, I found it easier to implement this on my own. In the end it took me about a day to get all the routes, UI, emails and everything working. I still need to enhance security a bit (reset counters to prevent abuse, etc.) but got the basics working:

  1. Created two new routes, /forgot and /reset, which don't require the user to be logged in to access.
  2. A GET on /forgot displays a UI with one input for email.
  3. A POST on /forgot checks that there is a user with that address and generates a random token.
    • Update the user's record with the token and expiry date
    • Send an email with a link to /reset/{token}
  4. A GET on /reset/{token} checks that there is a user with that token which hasn't expired then shows the UI with new password entry.
  5. A POST on /reset (sends new pwd and token) checks that there is a user with that token which hasn't expired.
    • Update the user's password.
    • Set the user's token and expiry date to null

Here's my code for generating a token (taken from node-password-reset):

function generateToken() {     var buf = new Buffer(16);     for (var i = 0; i < buf.length; i++) {         buf[i] = Math.floor(Math.random() * 256);     }     var id = buf.toString('base64');     return id; } 

Hope this helps.

EDIT: Here's the app.js. Note I'm keeping the entire user object in the session. I plan on moving to couchbase or similar in the future.

var express = require('express'); var path = require('path'); var favicon = require('static-favicon'); var flash = require('connect-flash'); var morgan = require('morgan'); var cookieParser = require('cookie-parser'); var cookieSession = require('cookie-session'); var bodyParser = require('body-parser'); var http = require('http'); var https = require('https'); var fs = require('fs'); var path = require('path'); var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy;  var app = express(); app.set('port', 3000); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade');  var cookies = cookieSession({     name: 'abc123',     secret: 'mysecret',     maxage: 10 * 60 * 1000 }); app.use(cookies); app.use(favicon()); app.use(flash()); app.use(morgan()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded()); app.use(cookieParser()); app.use(passport.initialize()); app.use(passport.session()); app.use(express.static(path.join(__dirname, 'public')));  module.exports = app;  passport.use(new LocalStrategy(function (username, password, done) {     return users.validateUser(username, password, done); }));  //KEEP ENTIRE USER OBJECT IN THE SESSION passport.serializeUser(function (user, done) {     done(null, user); }); passport.deserializeUser(function (user, done) {     done(null, user); });  //Error handling after everything else app.use(logErrors); //log all errors app.use(clientErrorHandler); //special handler for xhr app.use(errorHandler); //basic handler  http.createServer(app).listen(app.get('port'), function () {     console.log('Express server listening on HTTP port ' + app.get('port')); }); 

EDIT: Here are the routes.

app.get('/forgot', function (req, res) {     if (req.isAuthenticated()) {         //user is alreay logged in         return res.redirect('/');     }      //UI with one input for email     res.render('forgot'); });  app.post('/forgot', function (req, res) {     if (req.isAuthenticated()) {         //user is alreay logged in         return res.redirect('/');     }     users.forgot(req, res, function (err) {         if (err) {             req.flash('error', err);         }         else {             req.flash('success', 'Please check your email for further instructions.');         }         res.redirect('/');     }); });  app.get('/reset/:token', function (req, res) {     if (req.isAuthenticated()) {         //user is alreay logged in         return res.redirect('/');     }     var token = req.params.token;     users.checkReset(token, req, res, function (err, data) {         if (err)             req.flash('error', err);          //show the UI with new password entry         res.render('reset');     }); });  app.post('/reset', function (req, res) {     if (req.isAuthenticated()) {         //user is alreay logged in         return res.redirect('/');     }     users.reset(req, res, function (err) {         if (err) {             req.flash('error', err);             return res.redirect('/reset');         }         else {             req.flash('success', 'Password successfully reset.  Please login using new password.');             return res.redirect('/login');         }     }); }); 
like image 36
james Avatar answered Oct 13 '22 07:10

james