Skip to content

Instantly share code, notes, and snippets.

@nemtsov
Last active April 10, 2024 23:47
Show Gist options
  • Save nemtsov/6c2c24fa565a29404b487c61ce5bae4f to your computer and use it in GitHub Desktop.
Save nemtsov/6c2c24fa565a29404b487c61ce5bae4f to your computer and use it in GitHub Desktop.
Passport auth with password reset
const crypto = require('crypto');
const { promisify } = require('util');
const express = require('express');
const asyncify = require('express-asyncify');
const session = require('express-session');
const createFileStore = require('session-file-store');
const nodemailer = require('nodemailer');
const nodemailerSendgrid = require('nodemailer-sendgrid');
const bodyParser = require('body-parser');
const pass = require('passport');
const LocalStrategy = require('passport-local');
const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
const flash = require('connect-flash');
const templates = require('./templates-passport');
const PORT = 5000;
const SESSION_COOKIE_SECRET = '';
const SESSOIN_COOKIE_MAX_AGE_IN_MS = 60 * 60 * 1000;
const SESSION_COOKIE_IS_SECURE = false;
const GOOGLE_CLIENT_ID = '';
const GOOGLE_CLIENT_SECRET = '';
const SENDGRID_API_KEY = '';
const transport = nodemailer.createTransport(nodemailerSendgrid({
apiKey: SENDGRID_API_KEY,
}));
const users = [{
id: 'local/a0234aDdfj-2f4sdfa3oEerq-2U4',
fullName: 'A Ayevich',
email: 'hello@example.com',
password: 'password'
}];
pass.serializeUser((user, cb) => cb(null, user));
pass.deserializeUser((u, cb) => cb(null, u));
pass.use(new LocalStrategy({
usernameField: 'email',
}, (email, password, cb) => {
const user = users.find(u => u.email === email);
cb(null, (user && user.password === password) ? user : false);
}));
pass.use(new GoogleStrategy({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: `http://localhost:${PORT}/auth/google/callback`
}, (accessToken, refreshToken, profile, cb) => {
const user = {
id: `google/${profile.id}`,
email: profile.email,
fullName: profile.displayName,
profile,
tokens: { accessToken, refreshToken },
};
users.push(user);
cb(null, user);
}));
const app = asyncify(express());
const FileStore = createFileStore(session);
app.disable('x-powered-by');
app.use(session({
store: new FileStore(),
name: 'sid',
resave: false,
saveUninitialized: false,
secret: SESSION_COOKIE_SECRET,
cookie: {
maxAge: SESSOIN_COOKIE_MAX_AGE_IN_MS,
secure: SESSION_COOKIE_IS_SECURE,
sameSite: 'lax',
},
}));
app.use(flash());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(pass.initialize());
app.use(pass.session());
app.get('/', (req, res) => {
res.setHeader('Content-type', 'text/html');
res.end(templates.layout(`
${req.user ?
templates.loggedInGreeting(req.user) :
templates.loggedOut()}
`));
});
app.get('/login', (req, res) => {
res.setHeader('Content-type', 'text/html');
res.end(templates.layout(`
${templates.error(req.flash())}
${templates.loginForm()}
`));
});
app.get('/signup', (req, res) => {
res.setHeader('Content-type', 'text/html');
res.end(templates.layout(`
${templates.error(req.flash())}
${templates.signupForm()}
`));
});
app.post('/login', pass.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login',
failureFlash: true,
}));
app.get('/forgot', (req, res, next) => {
res.setHeader('Content-type', 'text/html');
res.end(templates.layout(`
${templates.error(req.flash())}
${templates.forgotPassword()}
`));
});
app.post('/forgot', async (req, res, next) => {
const token = (await promisify(crypto.randomBytes)(20)).toString('hex');
const user = users.find(u => u.email === req.body.email);
if (!user) {
req.flash('error', 'No account with that email address exists.');
return res.redirect('/forgot');
}
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000;
const resetEmail = {
to: user.email,
from: 'passwordreset@example.com',
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.
Please click on the following link, or paste this into your browser to complete the process:
http://${req.headers.host}/reset/${token}
If you did not request this, please ignore this email and your password will remain unchanged.
`,
};
await transport.sendMail(resetEmail);
req.flash('info', `An e-mail has been sent to ${user.email} with further instructions.`);
res.redirect('/forgot');
});
app.get('/reset/:token', (req, res) => {
const user = users.find(u => (
(u.resetPasswordExpires > Date.now()) &&
crypto.timingSafeEqual(Buffer.from(u.resetPasswordToken), Buffer.from(req.params.token))
));
if (!user) {
req.flash('error', 'Password reset token is invalid or has expired.');
return res.redirect('/forgot');
}
res.setHeader('Content-type', 'text/html');
res.end(templates.layout(`
${templates.error(req.flash())}
${templates.resetPassword(user.resetPasswordToken)}
`));
});
app.post('/reset/:token', async (req, res) => {
const user = users.find(u => (
(u.resetPasswordExpires > Date.now()) &&
crypto.timingSafeEqual(Buffer.from(u.resetPasswordToken), Buffer.from(req.params.token))
));
if (!user) {
req.flash('error', 'Password reset token is invalid or has expired.');
return res.redirect('/forgot');
}
user.password = req.body.password;
delete user.resetPasswordToken;
delete user.resetPasswordExpires;
const resetEmail = {
to: user.email,
from: 'passwordreset@example.com',
subject: 'Your password has been changed',
text: `
This is a confirmation that the password for your account "${user.email}" has just been changed.
`,
};
await transport.sendMail(resetEmail);
req.flash('success', `Success! Your password has been changed.`);
res.redirect('/');
});
app.get('/auth/google',
pass.authenticate('google', { scope: ['profile'] }));
app.get('/auth/google/callback',
pass.authenticate('google', { failureRedirect: '/login' }),
(req, res) => res.redirect('/'));
app.post('/signup', (req, res, next) => {
const user = {
id: 'local/a0234aDdfj-2f4sdfa3oEerq-2U4',
fullName: 'Boy Good',
email: req.body.email,
password: req.body.password,
};
users.push(user);
req.login(user, (err) => {
if (err) next(err);
else res.redirect('/');
});
});
app.listen(PORT, () => console.log(`on :${PORT}`));
@nemtsov
Copy link
Author

nemtsov commented Mar 27, 2020

@king9759 templates are a set of exported functions that return strings. The format looks like this:

module.exports = {
  layout(body) {
    return `
      <html>
        <body>
          ${body}
        </body>
      </html>
    `;
  }
};

And the app.post('/reset/:token', async (req, res) => { route is there to allow a user to reset their password, provided they send us a valid token; one that we generated in https://gist.github.com/nemtsov/6c2c24fa565a29404b487c61ce5bae4f#file-passport-auth-with-reset-js-L158

@manorinfinity
Copy link

Can you refactor the template lines with normal res.render ... It is Confusing me....... if I render reset file above the async function. How will I make it work, BY passing user.resetPasswordToken or what????

@yuxal
Copy link

yuxal commented May 4, 2020

Friendly suggestion:
move line 126:
const token = (await promisify(crypto.randomBytes)(20)).toString('hex');
below the
if (!user) { req.flash('error', 'Password reset token is invalid or has expired.'); return res.redirect('/forgot'); }
(waste of time creating token if no user)

@lqvinh2
Copy link

lqvinh2 commented Aug 25, 2020

Is there way using pass.use(new GoogleStrategy({...});, by input email and password in Nodejs, i need to get "accesstoken" ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment