mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 19:53:41 -05:00
Merge pull request #8882 from overleaf/jk-web-reject-same-password
[web] Password set/reset: reject current password GitOrigin-RevId: 2c40dda4926d9c68564ae5126b3393b9286bb661
This commit is contained in:
parent
7f6e2c971e
commit
d04ea76081
12 changed files with 148 additions and 36 deletions
|
@ -3,9 +3,11 @@ const Errors = require('../Errors/Errors')
|
||||||
class InvalidEmailError extends Errors.BackwardCompatibleError {}
|
class InvalidEmailError extends Errors.BackwardCompatibleError {}
|
||||||
class InvalidPasswordError extends Errors.BackwardCompatibleError {}
|
class InvalidPasswordError extends Errors.BackwardCompatibleError {}
|
||||||
class ParallelLoginError extends Errors.BackwardCompatibleError {}
|
class ParallelLoginError extends Errors.BackwardCompatibleError {}
|
||||||
|
class PasswordMustBeDifferentError extends Errors.BackwardCompatibleError {}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
InvalidEmailError,
|
InvalidEmailError,
|
||||||
InvalidPasswordError,
|
InvalidPasswordError,
|
||||||
ParallelLoginError,
|
ParallelLoginError,
|
||||||
|
PasswordMustBeDifferentError,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ const {
|
||||||
InvalidEmailError,
|
InvalidEmailError,
|
||||||
InvalidPasswordError,
|
InvalidPasswordError,
|
||||||
ParallelLoginError,
|
ParallelLoginError,
|
||||||
|
PasswordMustBeDifferentError,
|
||||||
} = require('./AuthenticationErrors')
|
} = require('./AuthenticationErrors')
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
const HaveIBeenPwned = require('./HaveIBeenPwned')
|
const HaveIBeenPwned = require('./HaveIBeenPwned')
|
||||||
|
@ -182,31 +183,45 @@ const AuthenticationManager = {
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
return callback(validationError)
|
return callback(validationError)
|
||||||
}
|
}
|
||||||
this.hashPassword(password, function (error, hash) {
|
// check if we can log in with this password. In which case we should reject it,
|
||||||
if (error) {
|
// because it is the same as the existing password.
|
||||||
return callback(error)
|
AuthenticationManager.authenticate(
|
||||||
}
|
{ _id: user._id },
|
||||||
db.users.updateOne(
|
password,
|
||||||
{
|
(err, authedUser) => {
|
||||||
_id: ObjectId(user._id.toString()),
|
if (err) {
|
||||||
},
|
return callback(err)
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
hashedPassword: hash,
|
|
||||||
},
|
|
||||||
$unset: {
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
function (updateError, result) {
|
|
||||||
if (updateError) {
|
|
||||||
return callback(updateError)
|
|
||||||
}
|
|
||||||
_checkWriteResult(result, callback)
|
|
||||||
HaveIBeenPwned.checkPasswordForReuseInBackground(password)
|
|
||||||
}
|
}
|
||||||
)
|
if (authedUser) {
|
||||||
})
|
return callback(new PasswordMustBeDifferentError())
|
||||||
|
}
|
||||||
|
this.hashPassword(password, function (error, hash) {
|
||||||
|
if (error) {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
db.users.updateOne(
|
||||||
|
{
|
||||||
|
_id: ObjectId(user._id.toString()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
hashedPassword: hash,
|
||||||
|
},
|
||||||
|
$unset: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
function (updateError, result) {
|
||||||
|
if (updateError) {
|
||||||
|
return callback(updateError)
|
||||||
|
}
|
||||||
|
_checkWriteResult(result, callback)
|
||||||
|
HaveIBeenPwned.checkPasswordForReuseInBackground(password)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
_passwordCharactersAreValid(password) {
|
_passwordCharactersAreValid(password) {
|
||||||
|
|
|
@ -78,6 +78,12 @@ async function setNewUserPassword(req, res, next) {
|
||||||
key: 'invalid-password',
|
key: 'invalid-password',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (error.name === 'PasswordMustBeDifferentError') {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: {
|
||||||
|
key: 'password-must-be-different',
|
||||||
|
},
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
message: req.i18n.translate('error_performing_request'),
|
message: req.i18n.translate('error_performing_request'),
|
||||||
|
|
|
@ -93,6 +93,11 @@ async function setNewUserPassword(token, password, auditLog) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reset = await AuthenticationManager.promises.setUserPassword(
|
||||||
|
user,
|
||||||
|
password
|
||||||
|
)
|
||||||
|
|
||||||
await UserAuditLogHandler.promises.addEntry(
|
await UserAuditLogHandler.promises.addEntry(
|
||||||
user._id,
|
user._id,
|
||||||
'reset-password',
|
'reset-password',
|
||||||
|
@ -100,11 +105,6 @@ async function setNewUserPassword(token, password, auditLog) {
|
||||||
auditLog.ip
|
auditLog.ip
|
||||||
)
|
)
|
||||||
|
|
||||||
const reset = await AuthenticationManager.promises.setUserPassword(
|
|
||||||
user,
|
|
||||||
password
|
|
||||||
)
|
|
||||||
|
|
||||||
return { found: true, reset, userId: user._id }
|
return { found: true, reset, userId: user._id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,12 @@ async function changePassword(req, res, next) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'InvalidPasswordError') {
|
if (error.name === 'InvalidPasswordError') {
|
||||||
return HttpErrorHandler.badRequest(req, res, error.message)
|
return HttpErrorHandler.badRequest(req, res, error.message)
|
||||||
|
} else if (error.name === 'PasswordMustBeDifferentError') {
|
||||||
|
return HttpErrorHandler.badRequest(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
req.i18n.translate('password_change_password_must_be_different')
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,11 @@ block content
|
||||||
+customFormMessage('invalid-password', 'danger')
|
+customFormMessage('invalid-password', 'danger')
|
||||||
| #{translate('invalid_password')}
|
| #{translate('invalid_password')}
|
||||||
|
|
||||||
|
+customFormMessage('password-must-be-different', 'danger')
|
||||||
|
| #{translate('password_change_password_must_be_different')}
|
||||||
|
br
|
||||||
|
a(href='/user/password/reset') #{translate("request_new_password_reset_email")}
|
||||||
|
|
||||||
div.alert.alert-success(
|
div.alert.alert-success(
|
||||||
hidden
|
hidden
|
||||||
role="alert"
|
role="alert"
|
||||||
|
|
|
@ -328,6 +328,7 @@
|
||||||
"not_found_error_from_the_supplied_url": "The link to open this content on Overleaf pointed to a file that could not be found. If this keeps happening for links on a particular site, please report this to them.",
|
"not_found_error_from_the_supplied_url": "The link to open this content on Overleaf pointed to a file that could not be found. If this keeps happening for links on a particular site, please report this to them.",
|
||||||
"too_many_requests": "Too many requests were received in a short space of time. Please wait for a few moments and try again.",
|
"too_many_requests": "Too many requests were received in a short space of time. Please wait for a few moments and try again.",
|
||||||
"password_change_passwords_do_not_match": "Passwords do not match",
|
"password_change_passwords_do_not_match": "Passwords do not match",
|
||||||
|
"password_change_password_must_be_different": "The password you entered is the same as your current password. Please try a different password.",
|
||||||
"password_change_old_password_wrong": "Your old password is wrong",
|
"password_change_old_password_wrong": "Your old password is wrong",
|
||||||
"github_for_link_shared_projects": "This project was accessed via link-sharing and won’t be synchronised with your GitHub unless you are invited via e-mail by the project owner.",
|
"github_for_link_shared_projects": "This project was accessed via link-sharing and won’t be synchronised with your GitHub unless you are invited via e-mail by the project owner.",
|
||||||
"browsing_project_latest_for_pseudo_label": "Browsing your project’s current state",
|
"browsing_project_latest_for_pseudo_label": "Browsing your project’s current state",
|
||||||
|
|
|
@ -234,6 +234,25 @@ describe('PasswordReset', function () {
|
||||||
const auditLog = userHelper.getAuditLogWithoutNoise()
|
const auditLog = userHelper.getAuditLogWithoutNoise()
|
||||||
expect(auditLog.length).to.equal(1)
|
expect(auditLog.length).to.equal(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('when the password is the same as current, should return 400 and not log the change', async function () {
|
||||||
|
// send reset request
|
||||||
|
response = await userHelper.request.post('/user/password/set', {
|
||||||
|
form: {
|
||||||
|
passwordResetToken: token,
|
||||||
|
password: userHelper.getDefaultPassword(),
|
||||||
|
},
|
||||||
|
simple: false,
|
||||||
|
})
|
||||||
|
expect(response.statusCode).to.equal(400)
|
||||||
|
expect(JSON.parse(response.body).message.key).to.equal(
|
||||||
|
'password-must-be-different'
|
||||||
|
)
|
||||||
|
userHelper = await UserHelper.getUser({ email })
|
||||||
|
|
||||||
|
const auditLog = userHelper.getAuditLogWithoutNoise()
|
||||||
|
expect(auditLog).to.deep.equal([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('without a valid token', function () {
|
describe('without a valid token', function () {
|
||||||
|
|
|
@ -256,7 +256,7 @@ describe('Sessions', function () {
|
||||||
|
|
||||||
// password reset from second session, should erase two of the three sessions
|
// password reset from second session, should erase two of the three sessions
|
||||||
next => {
|
next => {
|
||||||
this.user2.changePassword(err => next(err))
|
this.user2.changePassword(`password${Date.now()}`, err => next(err))
|
||||||
},
|
},
|
||||||
|
|
||||||
next => {
|
next => {
|
||||||
|
|
|
@ -152,7 +152,9 @@ class User {
|
||||||
this.setExtraAttributes(user)
|
this.setExtraAttributes(user)
|
||||||
AuthenticationManager.setUserPasswordInV2(user, this.password, error => {
|
AuthenticationManager.setUserPasswordInV2(user, this.password, error => {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return callback(error)
|
if (error.name !== 'PasswordMustBeDifferentError') {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.mongoUpdate({ $set: { emails: this.emails } }, error => {
|
this.mongoUpdate({ $set: { emails: this.emails } }, error => {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
|
@ -675,7 +677,7 @@ class User {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
changePassword(callback) {
|
changePassword(newPassword, callback) {
|
||||||
this.getCsrfToken(error => {
|
this.getCsrfToken(error => {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return callback(error)
|
return callback(error)
|
||||||
|
@ -685,11 +687,17 @@ class User {
|
||||||
url: '/user/password/update',
|
url: '/user/password/update',
|
||||||
json: {
|
json: {
|
||||||
currentPassword: this.password,
|
currentPassword: this.password,
|
||||||
newPassword1: this.password,
|
newPassword1: newPassword,
|
||||||
newPassword2: this.password,
|
newPassword2: newPassword,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
callback
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
this.password = newPassword
|
||||||
|
callback()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,9 @@ describe('AuthenticationManager', function () {
|
||||||
}
|
}
|
||||||
this.db.users.updateOne = sinon
|
this.db.users.updateOne = sinon
|
||||||
this.User.findOne = sinon.stub().callsArgWith(2, null, this.user)
|
this.User.findOne = sinon.stub().callsArgWith(2, null, this.user)
|
||||||
|
this.AuthenticationManager.authenticate = sinon
|
||||||
|
.stub()
|
||||||
|
.callsArgWith(2, null, null)
|
||||||
this.db.users.updateOne = sinon
|
this.db.users.updateOne = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(2, null, { modifiedCount: 1 })
|
.callsArgWith(2, null, { modifiedCount: 1 })
|
||||||
|
@ -614,6 +617,29 @@ describe('AuthenticationManager', function () {
|
||||||
this.bcrypt.hash = sinon.stub().callsArgWith(2, null, this.hashedPassword)
|
this.bcrypt.hash = sinon.stub().callsArgWith(2, null, this.hashedPassword)
|
||||||
this.User.findOne = sinon.stub().callsArgWith(2, null, this.user)
|
this.User.findOne = sinon.stub().callsArgWith(2, null, this.user)
|
||||||
this.db.users.updateOne = sinon.stub().callsArg(2)
|
this.db.users.updateOne = sinon.stub().callsArg(2)
|
||||||
|
this.AuthenticationManager.authenticate = sinon
|
||||||
|
.stub()
|
||||||
|
.callsArgWith(2, null, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('same as previous password', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.AuthenticationManager.authenticate = sinon
|
||||||
|
.stub()
|
||||||
|
.callsArgWith(2, null, this.user)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error', function (done) {
|
||||||
|
this.AuthenticationManager.setUserPassword(
|
||||||
|
this.user,
|
||||||
|
this.password,
|
||||||
|
err => {
|
||||||
|
expect(err).to.exist
|
||||||
|
expect(err.name).to.equal('PasswordMustBeDifferentError')
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('too long', function () {
|
describe('too long', function () {
|
||||||
|
|
|
@ -335,6 +335,30 @@ describe('PasswordResetHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('errors', function () {
|
describe('errors', function () {
|
||||||
|
describe('via setUserPassword', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.PasswordResetHandler.promises.getUserForPasswordResetToken =
|
||||||
|
sinon.stub().withArgs(this.token).resolves(this.user)
|
||||||
|
this.AuthenticationManager.promises.setUserPassword
|
||||||
|
.withArgs(this.user, this.password)
|
||||||
|
.rejects()
|
||||||
|
})
|
||||||
|
it('should return the error', function (done) {
|
||||||
|
this.PasswordResetHandler.setNewUserPassword(
|
||||||
|
this.token,
|
||||||
|
this.password,
|
||||||
|
this.auditLog,
|
||||||
|
(error, _result) => {
|
||||||
|
expect(error).to.exist
|
||||||
|
expect(
|
||||||
|
this.UserAuditLogHandler.promises.addEntry.callCount
|
||||||
|
).to.equal(0)
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('via UserAuditLogHandler', function () {
|
describe('via UserAuditLogHandler', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.PasswordResetHandler.promises.getUserForPasswordResetToken =
|
this.PasswordResetHandler.promises.getUserForPasswordResetToken =
|
||||||
|
@ -354,7 +378,7 @@ describe('PasswordResetHandler', function () {
|
||||||
this.UserAuditLogHandler.promises.addEntry.callCount
|
this.UserAuditLogHandler.promises.addEntry.callCount
|
||||||
).to.equal(1)
|
).to.equal(1)
|
||||||
expect(this.AuthenticationManager.promises.setUserPassword).to
|
expect(this.AuthenticationManager.promises.setUserPassword).to
|
||||||
.not.have.been.called
|
.have.been.called
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue