Merge pull request #10394 from overleaf/ii-password-reset-and-strength-checking

[web] Password reset strength checking and UI updates

GitOrigin-RevId: 442a5c9e7e9d0a61d3ae649f3526bc3c02fd5704
This commit is contained in:
ilkin-overleaf
2022-12-07 09:03:36 +00:00
committed by Copybot
parent 887ac13f06
commit 2675cab92e
21 changed files with 309 additions and 159 deletions
@@ -4,10 +4,12 @@ class InvalidEmailError extends Errors.BackwardCompatibleError {}
class InvalidPasswordError extends Errors.BackwardCompatibleError {}
class ParallelLoginError extends Errors.BackwardCompatibleError {}
class PasswordMustBeDifferentError extends Errors.BackwardCompatibleError {}
class PasswordReusedError extends Errors.BackwardCompatibleError {}
module.exports = {
InvalidEmailError,
InvalidPasswordError,
ParallelLoginError,
PasswordMustBeDifferentError,
PasswordReusedError,
}
@@ -8,6 +8,7 @@ const {
InvalidPasswordError,
ParallelLoginError,
PasswordMustBeDifferentError,
PasswordReusedError,
} = require('./AuthenticationErrors')
const util = require('util')
const HaveIBeenPwned = require('./HaveIBeenPwned')
@@ -256,8 +257,22 @@ const AuthenticationManager = {
if (match) {
return callback(new PasswordMustBeDifferentError())
}
this._setUserPasswordInMongo(user, password, callback)
HaveIBeenPwned.checkPasswordForReuseInBackground(password)
HaveIBeenPwned.checkPasswordForReuse(
password,
(error, isPasswordReused) => {
if (error) {
logger.err({ error }, 'cannot check password for re-use')
}
if (!error && isPasswordReused) {
return callback(new PasswordReusedError())
}
// password is strong enough or the validation with the service did not happen
this._setUserPasswordInMongo(user, password, callback)
}
)
}
)
},
@@ -5,6 +5,7 @@
happy path or via an error (message or attributes).
*/
const { callbackify } = require('util')
const fetch = require('node-fetch')
const crypto = require('crypto')
const Settings = require('@overleaf/settings')
@@ -89,29 +90,43 @@ async function isPasswordReused(password) {
return score > 0
}
function checkPasswordForReuseInBackground(password) {
async function checkPasswordForReuse(password) {
if (!Settings.apis.haveIBeenPwned.enabled) {
return
}
isPasswordReused(password)
.then(isReused => {
Metrics.inc('password_re_use', {
status: isReused ? 're-used' : 'unique',
})
})
.catch(err => {
// Make sure we do not leak any password details.
if (!CODED_ERROR_MESSAGES.includes(err.message)) {
err = new Error('hidden message')
}
err = new Error(err.message)
try {
const isReused = await isPasswordReused(password)
logger.err({ err }, 'cannot check password for re-use')
Metrics.inc('password_re_use', { status: 'failure' })
Metrics.inc('password_re_use', {
status: isReused ? 're-used' : 'unique',
})
return isReused
} catch (err) {
let error = err
// Make sure we do not leak any password details.
if (!CODED_ERROR_MESSAGES.includes(err.message)) {
error = new Error('hidden message')
}
error = new Error(error.message)
Metrics.inc('password_re_use', { status: 'failure' })
throw error
}
}
function checkPasswordForReuseInBackground(password) {
checkPasswordForReuse(password).catch(error => {
logger.err({ error }, 'cannot check password for re-use')
})
}
module.exports = {
checkPasswordForReuse: callbackify(checkPasswordForReuse),
checkPasswordForReuseInBackground,
promises: {
checkPasswordForReuse,
},
}
@@ -84,6 +84,12 @@ async function setNewUserPassword(req, res, next) {
key: 'password-must-be-different',
},
})
} else if (error.name === 'PasswordReusedError') {
return res.status(400).json({
message: {
key: 'password-must-be-strong',
},
})
} else {
return res.status(500).json({
message: req.i18n.translate('error_performing_request'),
@@ -103,6 +103,12 @@ async function changePassword(req, res, next) {
res,
req.i18n.translate('password_change_password_must_be_different')
)
} else if (error.name === 'PasswordReusedError') {
return res.status(400).json({
message: {
key: 'password-must-be-strong',
},
})
} else {
throw error
}