Skip to content
This repository has been archived by the owner on Feb 25, 2019. It is now read-only.

Commit

Permalink
feat(pwreset): Implement password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
adalinesimonian committed Aug 21, 2015
1 parent f30fc43 commit 184c559
Show file tree
Hide file tree
Showing 14 changed files with 481 additions and 17 deletions.
26 changes: 26 additions & 0 deletions email/passwordChanged.hogan
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<html>
<body style="background: #fafafa">
<style>
.button:hover {
background: #F07911 !important;
}
.button:active, .button:focus {
box-shadow: 0 0 0 !important;
margin: 2px 4px -2px 10px !important;
}
</style>
<div style="font: normal normal 400 14px Roboto, Noto, 'Helvetica Neue', Helvetica, Arial, sans-serif; background: #fafafa; color: #212121">
<p style="margin: 8px"><strong style="color: #757575">{{providerName}}</strong></p>
<h1 style="margin: 8px; font-size: 36px; color: #e65100">
Password changed
</h1>
<p style="margin: 8px">The password for your {{providerName}} account was recently changed.</p>
<p style="margin: 8px">If this was you, then everything worked OK and you've got nothing more to do.</p>
<p style="margin: 8px">If this was <strong>not</strong> you, your account may have been compromised. To regain control of your account, you'll need to<a href="{{recoveryURL}}" class="button"
style="display: block; display: inline-block; outline: none; width: 200px; text-align: center; margin: 0 8px; padding: 12px; box-shadow: 2px 2px 2px #757575; border-radius: 2px; -webkit-border-radius: 2px; background: #E37719; color: #fff; text-decoration: none; font-weight: 600">
Recover your account</a></p>
<p style="margin: 8px; font-size: 12px">If you don't see a link above, paste the following URL into your browser: {{recoveryURL}}</p>
<p style="margin: 8px; font-size: 12px">This e-mail was addressed to {{email}}</p>
</div>
</body>
</html>
26 changes: 26 additions & 0 deletions email/resetPassword.hogan
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<html>
<body style="background: #fafafa">
<style>
.button:hover {
background: #F07911 !important;
}
.button:active, .button:focus {
box-shadow: 0 0 0 !important;
margin: 2px 4px -2px 10px !important;
}
</style>
<div style="font: normal normal 400 14px Roboto, Noto, 'Helvetica Neue', Helvetica, Arial, sans-serif; background: #fafafa; color: #212121">
<p style="margin: 8px"><strong style="color: #757575">{{providerName}}</strong></p>
<h1 style="margin: 8px; font-size: 36px; color: #e65100">
Reset your password
</h1>
<p style="margin: 8px">We received a request to reset the password on your account.</p>
<p style="margin: 8px">If this was you, proceed to <a href="{{resetPasswordURL}}" class="button"
style="display: block; display: inline-block; outline: none; width: 200px; text-align: center; margin: 0 8px; padding: 12px; box-shadow: 2px 2px 2px #757575; border-radius: 2px; -webkit-border-radius: 2px; background: #E37719; color: #fff; text-decoration: none; font-weight: 600">
Reset your password</a></p>
<p style="margin: 8px; font-size: 12px">If you don't see a link above, paste the following URL into your browser: {{resetPasswordURL}}</p>
<p style="margin: 8px">If this wasn't you, ignore and/or delete this e-mail message.</p>
<p style="margin: 8px; font-size: 12px">This e-mail was addressed to {{email}}</p>
</div>
</body>
</html>
46 changes: 44 additions & 2 deletions models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,49 @@ User.prototype.verifyPassword = function (password, callback) {
};


/**
* Verify password strength
*/

User.verifyPasswordStrength = function (password) {
var daysToCrack = providers.password.daysToCrack;
if (CheckPassword(password) <= daysToCrack) {
return false;
}
return true;
}


/**
* Change password
*/

User.changePassword = function (id, password, callback) {
// require a password
if (!password) {
return callback(new PasswordRequiredError());
}

// require password to be strong
if (!User.verifyPasswordStrength(password)) {
return callback(new InsecurePasswordError());
}

User.patch(id,
{ password: password },
{ private: true },
function (err, user) {
if (err) { return callback(err); }
if (!user) { return callback(null, null); }

callback(null, user);
}
);
};




/**
* Create
*/
Expand All @@ -216,8 +259,7 @@ User.insert = function (data, options, callback) {
}

// check the password strength
var daysToCrack = providers.password.daysToCrack;
if (CheckPassword(data.password) <= daysToCrack) {
if (!User.verifyPasswordStrength(data.password)) {
return callback(new InsecurePasswordError());
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"pem-jwk": "^1.5.1",
"qs": "^4.0.0",
"redis": "^0.12.1",
"revalidator": "^0.3.1",
"superagent": "^1.2.0",
"yargs": "^3.8.0"
}
Expand Down
183 changes: 183 additions & 0 deletions routes/recovery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Module dependencies
*/

var url = require('url')
, revalidator = require('revalidator')
, settings = require('../boot/settings')
, mailer = require('../boot/mailer')
, User = require('../models/User')
, OneTimeToken = require('../models/OneTimeToken')
, PasswordsDisabledError = require('../errors/PasswordsDisabledError')
;



/**
* Account Recovery
*/

// Recovery flow:
//
// 1. Accept user e-mail
// 2. Validate input as valid e-mail, display error if not
// 3. Verify user account exists with specified e-mail
// 4. If user account exists, issue token and send e-mail
// 5. Regardless of success/failure, direct user to emailSent view
//
// Password reset flow:
//
// 1. When user clicks on link, verify token exists and is for pw reset
// 2. Direct user to resetPassword view
// 3. Verify password fulfills password strength requirements
// 4. If error, re-display resetPassword view, but with error message
// 5. Update user object and revoke token
// 6. Send user an e-mail notifying them that their password was changed
// 7. Direct user to passwordReset view
//
// Default expiry time for tokens is 1 day

function verifyPasswordsEnabled(req, res, next) {
if (!settings.providers.password) {
return next(new PasswordsDisabledError());
} else {
return next();
}
}

function verifyMailerConfigured(req, res, next) {
if (!mailer.transport) {
return next(new Error('Mailer not configured.'));
} else {
return next();
}
}

function verifyPasswordResetToken(req, res, next) {
if (!req.query.token) {
return res.render('recovery/resetPassword', {
error: 'Invalid reset code.'
});
}

OneTimeToken.peek(req.query.token, function (err, token) {
if (err) { return next(err); }
if (!token || token.use !== 'resetPassword') {
return res.render('recovery/resetPassword', {
error: 'Invalid reset code.'
});
}

req.passwordResetToken = token;
next();
});
}

module.exports = function (server) {
server.get('/recovery',
verifyPasswordsEnabled,
verifyMailerConfigured,
function (req, res, next) {
res.render('recovery/start');
}
);

server.post('/recovery',
verifyPasswordsEnabled,
verifyMailerConfigured,
function (req, res, next) {
if (
!req.body.email ||
!revalidator.validate.formats.email.test(req.body.email)
) {
return res.render('recovery/start', {
error: 'Please enter a valid e-mail address.'
});
}

User.getByEmail(req.body.email, function (err, user) {
if (err) { return next(err); }
if (!user) { return res.render('recovery/emailSent'); }

OneTimeToken.issue({
sub: user._id,
ttl: 3600 * 24, // 1 day
use: 'resetPassword'
}, function (err, token) {
if (err) { return next(err); }

var resetPasswordURL = url.parse(settings.issuer);
resetPasswordURL.pathname = 'resetPassword';
resetPasswordURL.query = { token: token._id };

mailer.sendMail('resetPassword', {
email: user.email,
resetPasswordURL: url.format(resetPasswordURL)
}, {
to: user.email,
subject: 'Reset your password'
}, function (err, responseStatus) {
// TODO: REQUIRES REFACTOR TO MAIL QUEUE
res.render('recovery/emailSent');
});
});
});
}
);

server.get('/resetPassword',
verifyPasswordsEnabled,
verifyMailerConfigured,
verifyPasswordResetToken,
function (req, res, next) {
res.render('recovery/resetPassword');
}
);

server.post('/resetPassword',
verifyPasswordsEnabled,
verifyMailerConfigured,
verifyPasswordResetToken,
function (req, res, next) {
var uid = req.passwordResetToken.sub;

if (req.body.password !== req.body.confirmPassword) {
return res.render('recovery/resetPassword', {
validationError: 'Passwords do not match.'
});
}

User.changePassword(uid, req.body.password, function (err, user) {

if (err && (
err.name === 'PasswordRequiredError' ||
err.name === 'InsecurePasswordError'
)) {
return res.render('recovery/resetPassword', {
validationError: err.message
});
}

if (err) { return next(err); }

OneTimeToken.revoke(req.passwordResetToken._id, function (err) {
if (err) { return next(err); }

var recoveryURL = url.parse(settings.issuer);
recoveryURL.pathname = 'recovery';

mailer.sendMail('passwordChanged', {
email: user.email,
recoveryURL: url.format(recoveryURL)
}, {
to: user.email,
subject: 'Your password has been changed'
}, function (err, responseStatus) {
// TODO: REQUIRES REFACTOR TO MAIL QUEUE
res.render('recovery/passwordReset');
});
});
});
}
);
};
28 changes: 16 additions & 12 deletions routes/signin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

var oidc = require('../oidc')
, settings = require('../boot/settings')
, mailer = require('../boot/mailer')
, passport = require('passport')
, crypto = require('crypto')
, qs = require('qs')
Expand Down Expand Up @@ -34,10 +35,11 @@ module.exports = function (server) {
oidc.verifyClient,
function (req, res, next) {
res.render('signin', {
params: qs.stringify(req.query),
request: req.query,
providers: settings.providers,
providerInfo: providerInfo
params: qs.stringify(req.query),
request: req.query,
providers: settings.providers,
providerInfo: providerInfo,
mailSupport: !!(mailer.transport)
});
});

Expand All @@ -59,19 +61,21 @@ module.exports = function (server) {
passport.authenticate(req.body.provider, function (err, user, info) {
if (err) {
res.render('signin', {
params: qs.stringify(req.body),
request: req.body,
providers: settings.providers,
params: qs.stringify(req.body),
request: req.body,
providers: settings.providers,
providerInfo: providerInfo,
error: err.message
mailSupport: !!(mailer.transport),
error: err.message
});
} else if (!user) {
res.render('signin', {
params: qs.stringify(req.body),
request: req.body,
providers: settings.providers,
params: qs.stringify(req.body),
request: req.body,
providers: settings.providers,
providerInfo: providerInfo,
formError: info.message
mailSupport: !!(mailer.transport),
formError: info.message
});
} else {
req.login(user, function (err) {
Expand Down
1 change: 1 addition & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require('./routes/userinfo')(server);
require('./routes/signin')(server);
require('./routes/signup')(server);
require('./routes/signout')(server);
require('./routes/recovery')(server);
require('./routes/resendEmail')(server);
require('./routes/verifyEmail')(server);
require('./routes/connect')(server);
Expand Down
Loading

0 comments on commit 184c559

Please sign in to comment.