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

假装没事ソ 提交于 2019-11-30 00:51:23

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.

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');
        }
    });
});

create a random reset key in your DB, persist it with a timestamp. then create a new route that accepts the reset key. verify the timestamp before changing the password to the new password from the route.

never tried this but i ran across this some time ago which is similar to what you need: https://github.com/substack/node-password-reset

Aleksandr Golovatyi
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!