This repository has been archived by the owner on Feb 25, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pwreset): Implement password reset
- Loading branch information
1 parent
f30fc43
commit 184c559
Showing
14 changed files
with
481 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); | ||
} | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.