Files
overleaf-cep/services/web/app/src/Features/PasswordReset/PasswordResetHandler.js
June Kelly 3288f87dbe [web] Password set/reset: reject current password (redux) (#8956)
* [web] set-password: reject same as current password

* [web] Add 'peek' operation on tokens

This allows us to improve the UX of the reset-password form,
by not invalidating the token in the case where the new
password will be rejected by validation logic.

We give up to three attempts before invalidating the token.

* [web] Add hide-on-error feature to async forms

This allows us to hide the form elements when certain
named error conditions occur.

* [web] reset-password: handle same-password rejection

We also change the implementation to use the new
peekValueFromToken API, and to expire the token explicitely
after it has been used to set the new password.

* [web] Validate OneTimeToken when loading password reset form

* [web] Rate limit GET: /user/password/set

Now that we are peeking at OneTimeToken when accessing this page,
we add rate to the GET request, matching that of the POST request.

* [web] Tidy up pug layout and mongo query for token peeking

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: 835205cc7c7ebe1209ee8e5b693efeb939a3056a
2022-09-28 08:06:54 +00:00

140 lines
3.6 KiB
JavaScript

const settings = require('@overleaf/settings')
const UserAuditLogHandler = require('../User/UserAuditLogHandler')
const UserGetter = require('../User/UserGetter')
const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler')
const EmailHandler = require('../Email/EmailHandler')
const AuthenticationManager = require('../Authentication/AuthenticationManager')
const { callbackify, promisify } = require('util')
function generateAndEmailResetToken(email, callback) {
UserGetter.getUserByAnyEmail(email, (err, user) => {
if (err || !user) {
return callback(err, null)
}
if (user.email !== email) {
return callback(null, 'secondary')
}
const data = { user_id: user._id.toString(), email }
OneTimeTokenHandler.getNewToken('password', data, (err, token) => {
if (err) {
return callback(err)
}
const emailOptions = {
to: email,
setNewPasswordUrl: `${
settings.siteUrl
}/user/password/set?passwordResetToken=${token}&email=${encodeURIComponent(
email
)}`,
}
EmailHandler.sendEmail('passwordResetRequested', emailOptions, err => {
if (err) {
return callback(err)
}
callback(null, 'primary')
})
})
})
}
function expirePasswordResetToken(token, callback) {
OneTimeTokenHandler.expireToken('password', token, err => {
return callback(err)
})
}
function getUserForPasswordResetToken(token, callback) {
OneTimeTokenHandler.peekValueFromToken(
'password',
token,
(err, data, remainingUses) => {
if (err != null) {
if (err.name === 'NotFoundError') {
return callback(null, null)
} else {
return callback(err)
}
}
if (data == null || data.email == null) {
return callback(null, null, remainingUses)
}
UserGetter.getUserByMainEmail(
data.email,
{ _id: 1, 'overleaf.id': 1, email: 1 },
(err, user) => {
if (err != null) {
callback(err)
} else if (user == null) {
callback(null, null, 0)
} else if (
data.user_id != null &&
data.user_id === user._id.toString()
) {
callback(null, user, remainingUses)
} else if (
data.v1_user_id != null &&
user.overleaf != null &&
data.v1_user_id === user.overleaf.id
) {
callback(null, user, remainingUses)
} else {
callback(null, null, 0)
}
}
)
}
)
}
async function setNewUserPassword(token, password, auditLog) {
const user = await PasswordResetHandler.promises.getUserForPasswordResetToken(
token
)
if (!user) {
return {
found: false,
reset: false,
userId: null,
}
}
await UserAuditLogHandler.promises.addEntry(
user._id,
'reset-password',
auditLog.initiatorId,
auditLog.ip
)
const reset = await AuthenticationManager.promises.setUserPassword(
user,
password
)
await PasswordResetHandler.promises.expirePasswordResetToken(token)
return { found: true, reset, userId: user._id }
}
const PasswordResetHandler = {
generateAndEmailResetToken,
setNewUserPassword: callbackify(setNewUserPassword),
getUserForPasswordResetToken,
expirePasswordResetToken,
}
PasswordResetHandler.promises = {
getUserForPasswordResetToken: promisify(
PasswordResetHandler.getUserForPasswordResetToken
),
expirePasswordResetToken: promisify(
PasswordResetHandler.expirePasswordResetToken
),
setNewUserPassword,
}
module.exports = PasswordResetHandler