mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #17408 from overleaf/dp-mongoose-callback-autherntication-manager
Promisify AuthenticationManager and AuthenticationManagerTests GitOrigin-RevId: 8120bf55d19380a6ecf5241ffab8722eff2d4fe3
This commit is contained in:
parent
7f23f59df6
commit
1ba5b27e57
2 changed files with 412 additions and 479 deletions
|
@ -10,7 +10,7 @@ const {
|
||||||
PasswordMustBeDifferentError,
|
PasswordMustBeDifferentError,
|
||||||
PasswordReusedError,
|
PasswordReusedError,
|
||||||
} = require('./AuthenticationErrors')
|
} = require('./AuthenticationErrors')
|
||||||
const util = require('util')
|
const { callbackify } = require('util')
|
||||||
const HaveIBeenPwned = require('./HaveIBeenPwned')
|
const HaveIBeenPwned = require('./HaveIBeenPwned')
|
||||||
const UserAuditLogHandler = require('../User/UserAuditLogHandler')
|
const UserAuditLogHandler = require('../User/UserAuditLogHandler')
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
|
@ -30,13 +30,9 @@ function _exceedsMaximumLengthRatio(password, maxSimilarity, value) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const _checkWriteResult = function (result, callback) {
|
const _checkWriteResult = function (result) {
|
||||||
// for MongoDB
|
// for MongoDB
|
||||||
if (result && result.modifiedCount === 1) {
|
return !!(result && result.modifiedCount === 1)
|
||||||
callback(null, true)
|
|
||||||
} else {
|
|
||||||
callback(null, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _validatePasswordNotTooLong(password) {
|
function _validatePasswordNotTooLong(password) {
|
||||||
|
@ -59,144 +55,129 @@ function _metricsForSuccessfulPasswordMatch(password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticationManager = {
|
const AuthenticationManager = {
|
||||||
_checkUserPassword(query, password, callback) {
|
async _checkUserPassword(query, password) {
|
||||||
// Using Mongoose for legacy reasons here. The returned User instance
|
// Using Mongoose for legacy reasons here. The returned User instance
|
||||||
// gets serialized into the session and there may be subtle differences
|
// gets serialized into the session and there may be subtle differences
|
||||||
// between the user returned by Mongoose vs mongodb (such as default values)
|
// between the user returned by Mongoose vs mongodb (such as default values)
|
||||||
User.findOne(query, (error, user) => {
|
const user = await User.findOne(query).exec()
|
||||||
if (error) {
|
|
||||||
return callback(error)
|
if (!user || !user.hashedPassword) {
|
||||||
}
|
return { user: null, match: null }
|
||||||
if (!user || !user.hashedPassword) {
|
}
|
||||||
return callback(null, null, null)
|
|
||||||
}
|
let rounds = 0
|
||||||
let rounds = 0
|
try {
|
||||||
try {
|
rounds = bcrypt.getRounds(user.hashedPassword)
|
||||||
rounds = bcrypt.getRounds(user.hashedPassword)
|
} catch (err) {
|
||||||
} catch (err) {
|
let prefix, suffix, length
|
||||||
let prefix, suffix, length
|
if (typeof user.hashedPassword === 'string') {
|
||||||
if (typeof user.hashedPassword === 'string') {
|
length = user.hashedPassword.length
|
||||||
length = user.hashedPassword.length
|
if (user.hashedPassword.length > 50) {
|
||||||
if (user.hashedPassword.length > 50) {
|
// A full bcrypt hash is 60 characters long.
|
||||||
// A full bcrypt hash is 60 characters long.
|
prefix = user.hashedPassword.slice(0, '$2a$12$x'.length)
|
||||||
prefix = user.hashedPassword.slice(0, '$2a$12$x'.length)
|
suffix = user.hashedPassword.slice(-4)
|
||||||
suffix = user.hashedPassword.slice(-4)
|
} else if (user.hashedPassword.length > 20) {
|
||||||
} else if (user.hashedPassword.length > 20) {
|
prefix = user.hashedPassword.slice(0, 4)
|
||||||
prefix = user.hashedPassword.slice(0, 4)
|
suffix = user.hashedPassword.slice(-4)
|
||||||
suffix = user.hashedPassword.slice(-4)
|
} else {
|
||||||
} else {
|
prefix = user.hashedPassword.slice(0, 4)
|
||||||
prefix = user.hashedPassword.slice(0, 4)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logger.warn(
|
}
|
||||||
{
|
logger.warn(
|
||||||
err,
|
{
|
||||||
userId: user._id,
|
err,
|
||||||
hashedPassword: {
|
userId: user._id,
|
||||||
type: typeof user.hashedPassword,
|
hashedPassword: {
|
||||||
length,
|
type: typeof user.hashedPassword,
|
||||||
prefix,
|
length,
|
||||||
suffix,
|
prefix,
|
||||||
},
|
suffix,
|
||||||
},
|
},
|
||||||
'unexpected user.hashedPassword value'
|
},
|
||||||
)
|
'unexpected user.hashedPassword value'
|
||||||
}
|
)
|
||||||
Metrics.inc('bcrypt', 1, {
|
}
|
||||||
method: 'compare',
|
Metrics.inc('bcrypt', 1, {
|
||||||
path: rounds,
|
method: 'compare',
|
||||||
})
|
path: rounds,
|
||||||
bcrypt.compare(password, user.hashedPassword, function (error, match) {
|
|
||||||
if (error) {
|
|
||||||
return callback(error)
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
_metricsForSuccessfulPasswordMatch(password)
|
|
||||||
}
|
|
||||||
callback(null, user, match)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const match = await bcrypt.compare(password, user.hashedPassword)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
_metricsForSuccessfulPasswordMatch(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, match }
|
||||||
},
|
},
|
||||||
|
|
||||||
authenticate(query, password, auditLog, { skipHIBPCheck = false }, callback) {
|
async authenticate(query, password, auditLog, { skipHIBPCheck = false }) {
|
||||||
AuthenticationManager._checkUserPassword(
|
const { user, match } = await AuthenticationManager._checkUserPassword(
|
||||||
query,
|
query,
|
||||||
password,
|
password
|
||||||
(error, user, match) => {
|
|
||||||
if (error) {
|
|
||||||
return callback(error)
|
|
||||||
}
|
|
||||||
if (!user) {
|
|
||||||
return callback(null, null)
|
|
||||||
}
|
|
||||||
const update = { $inc: { loginEpoch: 1 } }
|
|
||||||
if (!match) {
|
|
||||||
update.$set = { lastFailedLogin: new Date() }
|
|
||||||
}
|
|
||||||
User.updateOne(
|
|
||||||
{ _id: user._id, loginEpoch: user.loginEpoch },
|
|
||||||
update,
|
|
||||||
{},
|
|
||||||
(err, result) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err)
|
|
||||||
}
|
|
||||||
if (result.modifiedCount !== 1) {
|
|
||||||
return callback(new ParallelLoginError())
|
|
||||||
}
|
|
||||||
if (!match) {
|
|
||||||
if (!auditLog) {
|
|
||||||
return callback(null, null)
|
|
||||||
} else {
|
|
||||||
return UserAuditLogHandler.addEntry(
|
|
||||||
user._id,
|
|
||||||
'failed-password-match',
|
|
||||||
user._id,
|
|
||||||
auditLog.ipAddress,
|
|
||||||
auditLog.info,
|
|
||||||
err => {
|
|
||||||
if (err) {
|
|
||||||
logger.error(
|
|
||||||
{ userId: user._id, err, info: auditLog.info },
|
|
||||||
'Error while adding AuditLog entry for failed-password-match'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
callback(null, null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AuthenticationManager.checkRounds(
|
|
||||||
user,
|
|
||||||
user.hashedPassword,
|
|
||||||
password,
|
|
||||||
function (err) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err)
|
|
||||||
}
|
|
||||||
if (skipHIBPCheck) {
|
|
||||||
callback(null, user)
|
|
||||||
HaveIBeenPwned.checkPasswordForReuseInBackground(password)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
HaveIBeenPwned.checkPasswordForReuse(
|
|
||||||
password,
|
|
||||||
(err, isPasswordReused) => {
|
|
||||||
if (err) {
|
|
||||||
logger.err({ err }, 'cannot check password for re-use')
|
|
||||||
}
|
|
||||||
if (isPasswordReused) {
|
|
||||||
return callback(new PasswordReusedError())
|
|
||||||
}
|
|
||||||
callback(null, user)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = { $inc: { loginEpoch: 1 } }
|
||||||
|
if (!match) {
|
||||||
|
update.$set = { lastFailedLogin: new Date() }
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await User.updateOne(
|
||||||
|
{ _id: user._id, loginEpoch: user.loginEpoch },
|
||||||
|
update,
|
||||||
|
{}
|
||||||
|
).exec()
|
||||||
|
|
||||||
|
if (result.modifiedCount !== 1) {
|
||||||
|
throw new ParallelLoginError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
if (!auditLog) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await UserAuditLogHandler.promises.addEntry(
|
||||||
|
user._id,
|
||||||
|
'failed-password-match',
|
||||||
|
user._id,
|
||||||
|
auditLog.ipAddress,
|
||||||
|
auditLog.info
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
{ userId: user._id, err, info: auditLog.info },
|
||||||
|
'Error while adding AuditLog entry for failed-password-match'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await AuthenticationManager.checkRounds(user, user.hashedPassword, password)
|
||||||
|
|
||||||
|
if (skipHIBPCheck) {
|
||||||
|
HaveIBeenPwned.checkPasswordForReuseInBackground(password)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPasswordReused
|
||||||
|
try {
|
||||||
|
isPasswordReused = await HaveIBeenPwned.promises.checkPasswordForReuse(
|
||||||
|
password
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
logger.err({ err }, 'cannot check password for re-use')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPasswordReused) {
|
||||||
|
throw new PasswordReusedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
},
|
},
|
||||||
|
|
||||||
validateEmail(email) {
|
validateEmail(email) {
|
||||||
|
@ -295,108 +276,93 @@ const AuthenticationManager = {
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
setUserPassword(user, password, callback) {
|
async setUserPassword(user, password) {
|
||||||
AuthenticationManager.setUserPasswordInV2(user, password, callback)
|
return await AuthenticationManager.setUserPasswordInV2(user, password)
|
||||||
},
|
},
|
||||||
|
|
||||||
checkRounds(user, hashedPassword, password, callback) {
|
async checkRounds(user, hashedPassword, password) {
|
||||||
// Temporarily disable this function, TODO: re-enable this
|
// Temporarily disable this function, TODO: re-enable this
|
||||||
if (Settings.security.disableBcryptRoundsUpgrades) {
|
if (Settings.security.disableBcryptRoundsUpgrades) {
|
||||||
Metrics.inc('bcrypt_check_rounds', 1, { status: 'disabled' })
|
Metrics.inc('bcrypt_check_rounds', 1, { status: 'disabled' })
|
||||||
return callback()
|
return
|
||||||
}
|
}
|
||||||
// check current number of rounds and rehash if necessary
|
// check current number of rounds and rehash if necessary
|
||||||
const currentRounds = bcrypt.getRounds(hashedPassword)
|
const currentRounds = bcrypt.getRounds(hashedPassword)
|
||||||
if (currentRounds < BCRYPT_ROUNDS) {
|
if (currentRounds < BCRYPT_ROUNDS) {
|
||||||
Metrics.inc('bcrypt_check_rounds', 1, { status: 'upgrade' })
|
Metrics.inc('bcrypt_check_rounds', 1, { status: 'upgrade' })
|
||||||
AuthenticationManager._setUserPasswordInMongo(user, password, callback)
|
return await AuthenticationManager._setUserPasswordInMongo(user, password)
|
||||||
} else {
|
} else {
|
||||||
Metrics.inc('bcrypt_check_rounds', 1, { status: 'success' })
|
Metrics.inc('bcrypt_check_rounds', 1, { status: 'success' })
|
||||||
callback()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hashPassword(password, callback) {
|
async hashPassword(password) {
|
||||||
// Double-check the size to avoid truncating in bcrypt.
|
// Double-check the size to avoid truncating in bcrypt.
|
||||||
const error = _validatePasswordNotTooLong(password)
|
const error = _validatePasswordNotTooLong(password)
|
||||||
if (error) {
|
if (error) {
|
||||||
return callback(error)
|
throw error
|
||||||
}
|
}
|
||||||
bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) {
|
|
||||||
if (error) {
|
const salt = await bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION)
|
||||||
return callback(error)
|
|
||||||
}
|
Metrics.inc('bcrypt', 1, {
|
||||||
Metrics.inc('bcrypt', 1, {
|
method: 'hash',
|
||||||
method: 'hash',
|
path: BCRYPT_ROUNDS,
|
||||||
path: BCRYPT_ROUNDS,
|
|
||||||
})
|
|
||||||
bcrypt.hash(password, salt, callback)
|
|
||||||
})
|
})
|
||||||
|
return await bcrypt.hash(password, salt)
|
||||||
},
|
},
|
||||||
|
|
||||||
setUserPasswordInV2(user, password, callback) {
|
async setUserPasswordInV2(user, password) {
|
||||||
if (!user || !user.email || !user._id) {
|
if (!user || !user.email || !user._id) {
|
||||||
return callback(new Error('invalid user object'))
|
throw new Error('invalid user object')
|
||||||
}
|
}
|
||||||
const validationError = this.validatePassword(password, user.email)
|
const validationError = this.validatePassword(password, user.email)
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
return callback(validationError)
|
throw validationError
|
||||||
}
|
}
|
||||||
// check if we can log in with this password. In which case we should reject it,
|
// check if we can log in with this password. In which case we should reject it,
|
||||||
// because it is the same as the existing password.
|
// because it is the same as the existing password.
|
||||||
AuthenticationManager._checkUserPassword(
|
const { match } = await AuthenticationManager._checkUserPassword(
|
||||||
{ _id: user._id },
|
{ _id: user._id },
|
||||||
password,
|
password
|
||||||
(err, _user, match) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err)
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
return callback(new PasswordMustBeDifferentError())
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
throw new PasswordMustBeDifferentError()
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPasswordReused
|
||||||
|
try {
|
||||||
|
isPasswordReused = await HaveIBeenPwned.promises.checkPasswordForReuse(
|
||||||
|
password
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
logger.err({ error }, 'cannot check password for re-use')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPasswordReused) {
|
||||||
|
throw new PasswordReusedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// password is strong enough or the validation with the service did not happen
|
||||||
|
return await this._setUserPasswordInMongo(user, password)
|
||||||
},
|
},
|
||||||
|
|
||||||
_setUserPasswordInMongo(user, password, callback) {
|
async _setUserPasswordInMongo(user, password) {
|
||||||
this.hashPassword(password, function (error, hash) {
|
const hash = await this.hashPassword(password)
|
||||||
if (error) {
|
const result = await db.users.updateOne(
|
||||||
return callback(error)
|
{ _id: new ObjectId(user._id.toString()) },
|
||||||
}
|
{
|
||||||
db.users.updateOne(
|
$set: {
|
||||||
{ _id: new ObjectId(user._id.toString()) },
|
hashedPassword: hash,
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
hashedPassword: hash,
|
|
||||||
},
|
|
||||||
$unset: {
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
function (updateError, result) {
|
$unset: {
|
||||||
if (updateError) {
|
password: true,
|
||||||
return callback(updateError)
|
},
|
||||||
}
|
}
|
||||||
_checkWriteResult(result, callback)
|
)
|
||||||
}
|
|
||||||
)
|
return _checkWriteResult(result)
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_passwordCharactersAreValid(password) {
|
_passwordCharactersAreValid(password) {
|
||||||
|
@ -500,10 +466,17 @@ const AuthenticationManager = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationManager.promises = {
|
module.exports = {
|
||||||
authenticate: util.promisify(AuthenticationManager.authenticate),
|
_validatePasswordNotTooSimilar:
|
||||||
hashPassword: util.promisify(AuthenticationManager.hashPassword),
|
AuthenticationManager._validatePasswordNotTooSimilar, // Private function exported for tests
|
||||||
setUserPassword: util.promisify(AuthenticationManager.setUserPassword),
|
validateEmail: AuthenticationManager.validateEmail,
|
||||||
|
validatePassword: AuthenticationManager.validatePassword,
|
||||||
|
getMessageForInvalidPasswordError:
|
||||||
|
AuthenticationManager.getMessageForInvalidPasswordError,
|
||||||
|
authenticate: callbackify(AuthenticationManager.authenticate),
|
||||||
|
setUserPassword: callbackify(AuthenticationManager.setUserPassword),
|
||||||
|
checkRounds: callbackify(AuthenticationManager.checkRounds),
|
||||||
|
hashPassword: callbackify(AuthenticationManager.hashPassword),
|
||||||
|
setUserPasswordInV2: callbackify(AuthenticationManager.setUserPasswordInV2),
|
||||||
|
promises: AuthenticationManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AuthenticationManager
|
|
||||||
|
|
|
@ -14,14 +14,18 @@ describe('AuthenticationManager', function () {
|
||||||
this.settings = { security: { bcryptRounds: 4 } }
|
this.settings = { security: { bcryptRounds: 4 } }
|
||||||
this.metrics = { inc: sinon.stub().returns() }
|
this.metrics = { inc: sinon.stub().returns() }
|
||||||
this.HaveIBeenPwned = {
|
this.HaveIBeenPwned = {
|
||||||
checkPasswordForReuse: sinon.stub().yields(null, false),
|
promises: {
|
||||||
|
checkPasswordForReuse: sinon.stub().resolves(false),
|
||||||
|
},
|
||||||
checkPasswordForReuseInBackground: sinon.stub(),
|
checkPasswordForReuseInBackground: sinon.stub(),
|
||||||
}
|
}
|
||||||
this.AuthenticationManager = SandboxedModule.require(modulePath, {
|
this.AuthenticationManager = SandboxedModule.require(modulePath, {
|
||||||
requires: {
|
requires: {
|
||||||
'../../models/User': {
|
'../../models/User': {
|
||||||
User: (this.User = {
|
User: (this.User = {
|
||||||
updateOne: sinon.stub().callsArgWith(3, null, { modifiedCount: 1 }),
|
updateOne: sinon
|
||||||
|
.stub()
|
||||||
|
.returns({ exec: sinon.stub().resolves({ modifiedCount: 1 }) }),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'../../infrastructure/mongodb': {
|
'../../infrastructure/mongodb': {
|
||||||
|
@ -32,16 +36,17 @@ describe('AuthenticationManager', function () {
|
||||||
getRounds: sinon.stub().returns(4),
|
getRounds: sinon.stub().returns(4),
|
||||||
}),
|
}),
|
||||||
'@overleaf/settings': this.settings,
|
'@overleaf/settings': this.settings,
|
||||||
'../User/UserGetter': (this.UserGetter = {}),
|
'../User/UserGetter': (this.UserGetter = { promises: {} }),
|
||||||
'./AuthenticationErrors': AuthenticationErrors,
|
'./AuthenticationErrors': AuthenticationErrors,
|
||||||
'./HaveIBeenPwned': this.HaveIBeenPwned,
|
'./HaveIBeenPwned': this.HaveIBeenPwned,
|
||||||
'../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
|
'../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
|
||||||
addEntry: sinon.stub().callsArgWith(5, null),
|
promises: {
|
||||||
|
addEntry: sinon.stub().resolves(null),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
'@overleaf/metrics': this.metrics,
|
'@overleaf/metrics': this.metrics,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.callback = sinon.stub()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
@ -67,22 +72,20 @@ describe('AuthenticationManager', function () {
|
||||||
email: (this.email = 'USER@overleaf.com'),
|
email: (this.email = 'USER@overleaf.com'),
|
||||||
}
|
}
|
||||||
this.user.hashedPassword = this.testPassword
|
this.user.hashedPassword = this.testPassword
|
||||||
this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
|
this.User.findOne = sinon
|
||||||
|
.stub()
|
||||||
|
.returns({ exec: sinon.stub().resolves(this.user) })
|
||||||
this.metrics.inc.reset()
|
this.metrics.inc.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the hashed password matches', function () {
|
describe('when the hashed password matches', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
this.unencryptedPassword = 'testpassword'
|
this.unencryptedPassword = 'testpassword'
|
||||||
this.AuthenticationManager.authenticate(
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencryptedPassword,
|
this.unencryptedPassword,
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
(error, user) => {
|
|
||||||
this.callback(error, user)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -104,7 +107,7 @@ describe('AuthenticationManager', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return the user', function () {
|
it('should return the user', function () {
|
||||||
this.callback.should.have.been.calledWith(null, this.user)
|
this.result.should.equal(this.user)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should send metrics', function () {
|
it('should send metrics', function () {
|
||||||
|
@ -115,16 +118,12 @@ describe('AuthenticationManager', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the encrypted passwords do not match', function () {
|
describe('when the encrypted passwords do not match', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
this.AuthenticationManager.authenticate(
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
'notthecorrectpassword',
|
'notthecorrectpassword',
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
(...args) => {
|
|
||||||
this.callback(...args)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -142,7 +141,7 @@ describe('AuthenticationManager', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not return the user', function () {
|
it('should not return the user', function () {
|
||||||
this.callback.calledWith(null, null).should.equal(true)
|
expect(this.result).to.equal(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -150,50 +149,37 @@ describe('AuthenticationManager', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.User.updateOne = sinon
|
this.User.updateOne = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(3, null, { modifiedCount: 0 })
|
.returns({ exec: sinon.stub().resolves({ modifiedCount: 0 }) })
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('correct password', function () {
|
describe('correct password', function () {
|
||||||
beforeEach(function (done) {
|
it('should return an error', async function () {
|
||||||
this.AuthenticationManager.authenticate(
|
await expect(
|
||||||
{ email: this.email },
|
this.AuthenticationManager.promises.authenticate(
|
||||||
'testpassword',
|
{ email: this.email },
|
||||||
null,
|
'testpassword',
|
||||||
{ skipHIBPCheck: true },
|
null,
|
||||||
(...args) => {
|
{ skipHIBPCheck: true }
|
||||||
this.callback(...args)
|
)
|
||||||
done()
|
).to.be.rejectedWith(AuthenticationErrors.ParallelLoginError)
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return an error', function () {
|
|
||||||
this.callback.should.have.been.calledWith(
|
|
||||||
sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('bad password', function () {
|
describe('bad password', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(function () {
|
||||||
this.User.updateOne = sinon
|
this.User.updateOne = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.yields(null, { modifiedCount: 0 })
|
.returns({ exec: sinon.stub().resolves({ modifiedCount: 0 }) })
|
||||||
this.AuthenticationManager.authenticate(
|
|
||||||
{ email: this.email },
|
|
||||||
'notthecorrectpassword',
|
|
||||||
null,
|
|
||||||
{ skipHIBPCheck: true },
|
|
||||||
(...args) => {
|
|
||||||
this.callback(...args)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
it('should return an error', function () {
|
it('should return an error', async function () {
|
||||||
this.callback.should.have.been.calledWith(
|
await expect(
|
||||||
sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
|
this.AuthenticationManager.promises.authenticate(
|
||||||
)
|
{ email: this.email },
|
||||||
|
'notthecorrectpassword',
|
||||||
|
null,
|
||||||
|
{ skipHIBPCheck: true }
|
||||||
|
)
|
||||||
|
).to.be.rejectedWith(AuthenticationErrors.ParallelLoginError)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -206,50 +192,41 @@ describe('AuthenticationManager', function () {
|
||||||
email: (this.email = 'USER@overleaf.com'),
|
email: (this.email = 'USER@overleaf.com'),
|
||||||
}
|
}
|
||||||
this.db.users.updateOne = sinon
|
this.db.users.updateOne = sinon
|
||||||
this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
|
this.User.findOne = sinon
|
||||||
this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
|
|
||||||
this.db.users.updateOne = sinon
|
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(2, null, { modifiedCount: 1 })
|
.returns({ exec: sinon.stub().resolves(this.user) })
|
||||||
|
this.bcrypt.compare = sinon.stub().resolves(false)
|
||||||
|
this.db.users.updateOne = sinon.stub().resolves({ modifiedCount: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not produce an error', function (done) {
|
it('should not produce an error', async function () {
|
||||||
this.AuthenticationManager.setUserPasswordInV2(
|
const updated =
|
||||||
this.user,
|
await this.AuthenticationManager.promises.setUserPasswordInV2(
|
||||||
'testpassword',
|
this.user,
|
||||||
(err, updated) => {
|
'testpassword'
|
||||||
expect(err).to.not.exist
|
)
|
||||||
expect(updated).to.equal(true)
|
expect(updated).to.equal(true)
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should set the hashed password', function (done) {
|
it('should set the hashed password', async function () {
|
||||||
this.AuthenticationManager.setUserPasswordInV2(
|
await this.AuthenticationManager.promises.setUserPasswordInV2(
|
||||||
this.user,
|
this.user,
|
||||||
'testpassword',
|
'testpassword'
|
||||||
err => {
|
|
||||||
expect(err).to.not.exist
|
|
||||||
const { hashedPassword } =
|
|
||||||
this.db.users.updateOne.lastCall.args[1].$set
|
|
||||||
expect(hashedPassword).to.exist
|
|
||||||
expect(hashedPassword.length).to.equal(60)
|
|
||||||
expect(hashedPassword).to.match(/^\$2a\$04\$[a-zA-Z0-9/.]{53}$/)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { hashedPassword } = this.db.users.updateOne.lastCall.args[1].$set
|
||||||
|
expect(hashedPassword).to.exist
|
||||||
|
expect(hashedPassword.length).to.equal(60)
|
||||||
|
expect(hashedPassword).to.match(/^\$2a\$04\$[a-zA-Z0-9/.]{53}$/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('hashPassword', function () {
|
describe('hashPassword', function () {
|
||||||
it('should block too long passwords', function (done) {
|
it('should block too long passwords', async function () {
|
||||||
this.AuthenticationManager.hashPassword('x'.repeat(100), err => {
|
await expect(
|
||||||
expect(err).to.exist
|
this.AuthenticationManager.promises.hashPassword('x'.repeat(100))
|
||||||
expect(err.message).to.equal('password is too long')
|
).to.be.rejectedWith('password is too long')
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -261,24 +238,22 @@ describe('AuthenticationManager', function () {
|
||||||
email: (this.email = 'USER@overleaf.com'),
|
email: (this.email = 'USER@overleaf.com'),
|
||||||
}
|
}
|
||||||
this.unencryptedPassword = 'banana'
|
this.unencryptedPassword = 'banana'
|
||||||
this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
|
this.User.findOne = sinon
|
||||||
|
.stub()
|
||||||
|
.returns({ exec: sinon.stub().resolves(this.user) })
|
||||||
this.metrics.inc.reset()
|
this.metrics.inc.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the hashed password matches', function () {
|
describe('when the hashed password matches', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
||||||
this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
|
this.bcrypt.compare = sinon.stub().resolves(true)
|
||||||
this.bcrypt.getRounds = sinon.stub().returns(4)
|
this.bcrypt.getRounds = sinon.stub().returns(4)
|
||||||
this.AuthenticationManager.authenticate(
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencryptedPassword,
|
this.unencryptedPassword,
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
(error, user) => {
|
|
||||||
this.callback(error, user)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -301,70 +276,65 @@ describe('AuthenticationManager', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return the user', function () {
|
it('should return the user', function () {
|
||||||
this.callback.calledWith(null, this.user).should.equal(true)
|
this.result.should.equal(this.user)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('HIBP', function () {
|
describe('HIBP', function () {
|
||||||
it('should not check HIBP if not requested', function () {
|
it('should not check HIBP if not requested', function () {
|
||||||
this.HaveIBeenPwned.checkPasswordForReuse.should.not.have.been
|
this.HaveIBeenPwned.promises.checkPasswordForReuse.should.not.have
|
||||||
.called
|
.been.called
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should check HIBP if requested', function (done) {
|
it('should check HIBP if requested', async function () {
|
||||||
this.AuthenticationManager.authenticate(
|
await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencryptedPassword,
|
this.unencryptedPassword,
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: false },
|
{ skipHIBPCheck: false }
|
||||||
error => {
|
)
|
||||||
if (error) return done(error)
|
|
||||||
this.HaveIBeenPwned.checkPasswordForReuse.should.have.been.calledWith(
|
this.HaveIBeenPwned.promises.checkPasswordForReuse.should.have.been.calledWith(
|
||||||
this.unencryptedPassword
|
this.unencryptedPassword
|
||||||
)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the encrypted passwords do not match', function () {
|
describe('when the encrypted passwords do not match', function () {
|
||||||
beforeEach(function () {
|
beforeEach(async function () {
|
||||||
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
||||||
this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
|
this.bcrypt.compare = sinon.stub().resolves(false)
|
||||||
this.AuthenticationManager.authenticate(
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencryptedPassword,
|
this.unencryptedPassword,
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
this.callback
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not return the user', function () {
|
it('should not return the user', function () {
|
||||||
this.callback.calledWith(null, null).should.equal(true)
|
expect(this.result).to.equal(null)
|
||||||
this.UserAuditLogHandler.addEntry.callCount.should.equal(0)
|
this.UserAuditLogHandler.promises.addEntry.callCount.should.equal(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the encrypted passwords do not match, with auditLog', function () {
|
describe('when the encrypted passwords do not match, with auditLog', function () {
|
||||||
beforeEach(function () {
|
beforeEach(async function () {
|
||||||
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
||||||
this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
|
this.bcrypt.compare = sinon.stub().resolves(false)
|
||||||
this.auditLog = { ipAddress: 'ip', info: { method: 'foo' } }
|
this.auditLog = { ipAddress: 'ip', info: { method: 'foo' } }
|
||||||
this.AuthenticationManager.authenticate(
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencryptedPassword,
|
this.unencryptedPassword,
|
||||||
this.auditLog,
|
this.auditLog,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
this.callback
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not return the user, but add entry to audit log', function () {
|
it('should not return the user, but add entry to audit log', function () {
|
||||||
this.callback.calledWith(null, null).should.equal(true)
|
expect(this.result).to.equal(null)
|
||||||
this.UserAuditLogHandler.addEntry.callCount.should.equal(1)
|
this.UserAuditLogHandler.promises.addEntry.callCount.should.equal(1)
|
||||||
this.UserAuditLogHandler.addEntry
|
this.UserAuditLogHandler.promises.addEntry
|
||||||
.calledWith(
|
.calledWith(
|
||||||
this.user._id,
|
this.user._id,
|
||||||
'failed-password-match',
|
'failed-password-match',
|
||||||
|
@ -377,22 +347,18 @@ describe('AuthenticationManager', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the hashed password matches but the number of rounds is too low', function () {
|
describe('when the hashed password matches but the number of rounds is too low', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
||||||
this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
|
this.bcrypt.compare = sinon.stub().resolves(true)
|
||||||
this.bcrypt.getRounds = sinon.stub().returns(1)
|
this.bcrypt.getRounds = sinon.stub().returns(1)
|
||||||
this.AuthenticationManager._setUserPasswordInMongo = sinon
|
this.AuthenticationManager.promises._setUserPasswordInMongo = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(2, null)
|
.resolves()
|
||||||
this.AuthenticationManager.authenticate(
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencryptedPassword,
|
this.unencryptedPassword,
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
(error, user) => {
|
|
||||||
this.callback(error, user)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -415,34 +381,30 @@ describe('AuthenticationManager', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should set the users password (with a higher number of rounds)', function () {
|
it('should set the users password (with a higher number of rounds)', function () {
|
||||||
this.AuthenticationManager._setUserPasswordInMongo
|
this.AuthenticationManager.promises._setUserPasswordInMongo
|
||||||
.calledWith(this.user, this.unencryptedPassword)
|
.calledWith(this.user, this.unencryptedPassword)
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return the user', function () {
|
it('should return the user', function () {
|
||||||
this.callback.calledWith(null, this.user).should.equal(true)
|
this.result.should.equal(this.user)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the hashed password matches but the number of rounds is too low, but upgrades disabled', function () {
|
describe('when the hashed password matches but the number of rounds is too low, but upgrades disabled', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
this.settings.security.disableBcryptRoundsUpgrades = true
|
this.settings.security.disableBcryptRoundsUpgrades = true
|
||||||
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
|
||||||
this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
|
this.bcrypt.compare = sinon.stub().resolves(true)
|
||||||
this.bcrypt.getRounds = sinon.stub().returns(1)
|
this.bcrypt.getRounds = sinon.stub().returns(1)
|
||||||
this.AuthenticationManager.setUserPassword = sinon
|
this.AuthenticationManager.promises.setUserPassword = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(2, null)
|
.resolves()
|
||||||
this.AuthenticationManager.authenticate(
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencryptedPassword,
|
this.unencryptedPassword,
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
(error, user) => {
|
|
||||||
this.callback(error, user)
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -455,31 +417,32 @@ describe('AuthenticationManager', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not set the users password (with a higher number of rounds)', function () {
|
it('should not set the users password (with a higher number of rounds)', function () {
|
||||||
this.AuthenticationManager.setUserPassword
|
this.AuthenticationManager.promises.setUserPassword
|
||||||
.calledWith(this.user, this.unencryptedPassword)
|
.calledWith(this.user, this.unencryptedPassword)
|
||||||
.should.equal(false)
|
.should.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return the user', function () {
|
it('should return the user', function () {
|
||||||
this.callback.calledWith(null, this.user).should.equal(true)
|
this.result.should.equal(this.user)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the user does not exist in the database', function () {
|
describe('when the user does not exist in the database', function () {
|
||||||
beforeEach(function () {
|
beforeEach(async function () {
|
||||||
this.User.findOne = sinon.stub().callsArgWith(1, null, null)
|
this.User.findOne = sinon
|
||||||
this.AuthenticationManager.authenticate(
|
.stub()
|
||||||
|
.returns({ exec: sinon.stub().resolves(null) })
|
||||||
|
this.result = await this.AuthenticationManager.promises.authenticate(
|
||||||
{ email: this.email },
|
{ email: this.email },
|
||||||
this.unencrpytedPassword,
|
this.unencrpytedPassword,
|
||||||
null,
|
null,
|
||||||
{ skipHIBPCheck: true },
|
{ skipHIBPCheck: true }
|
||||||
this.callback
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not return a user', function () {
|
it('should not return a user', function () {
|
||||||
this.callback.calledWith(null, null).should.equal(true)
|
expect(this.result).to.equal(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -799,28 +762,27 @@ describe('AuthenticationManager', function () {
|
||||||
email: 'user@example.com',
|
email: 'user@example.com',
|
||||||
hashedPassword: this.hashedPassword,
|
hashedPassword: this.hashedPassword,
|
||||||
}
|
}
|
||||||
this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
|
this.bcrypt.compare = sinon.stub().resolves(false)
|
||||||
this.bcrypt.genSalt = sinon.stub().callsArgWith(2, null, this.salt)
|
this.bcrypt.genSalt = sinon.stub().resolves(this.salt)
|
||||||
this.bcrypt.hash = sinon.stub().callsArgWith(2, null, this.hashedPassword)
|
this.bcrypt.hash = sinon.stub().resolves(this.hashedPassword)
|
||||||
this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
|
this.User.findOne = sinon
|
||||||
this.db.users.updateOne = sinon.stub().callsArg(2)
|
.stub()
|
||||||
|
.returns({ exec: sinon.stub().resolves(this.user) })
|
||||||
|
this.db.users.updateOne = sinon.stub().resolves()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('same as previous password', function () {
|
describe('same as previous password', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.bcrypt.compare.callsArgWith(2, null, true)
|
this.bcrypt.compare.resolves(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an error', function (done) {
|
it('should be rejected', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
await expect(
|
||||||
this.user,
|
this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
err => {
|
this.password
|
||||||
expect(err).to.exist
|
)
|
||||||
expect(err.name).to.equal('PasswordMustBeDifferentError')
|
).to.be.rejectedWith(AuthenticationErrors.PasswordMustBeDifferentError)
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -834,27 +796,25 @@ describe('AuthenticationManager', function () {
|
||||||
this.password = 'dsdsadsadsadsadsadkjsadjsadjsadljs'
|
this.password = 'dsdsadsadsadsadsadkjsadjsadjsadljs'
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return and error', function (done) {
|
it('should return and error', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
await expect(
|
||||||
this.user,
|
this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
err => {
|
this.password
|
||||||
expect(err).to.exist
|
)
|
||||||
done()
|
).to.be.rejectedWith('password is too long')
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not start the bcrypt process', function (done) {
|
it('should not start the bcrypt process', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
await expect(
|
||||||
this.user,
|
this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
() => {
|
this.password
|
||||||
this.bcrypt.genSalt.called.should.equal(false)
|
)
|
||||||
this.bcrypt.hash.called.should.equal(false)
|
).to.be.rejected
|
||||||
done()
|
|
||||||
}
|
this.bcrypt.genSalt.called.should.equal(false)
|
||||||
)
|
this.bcrypt.hash.called.should.equal(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -863,16 +823,13 @@ describe('AuthenticationManager', function () {
|
||||||
this.password = `some${this.user.email}password`
|
this.password = `some${this.user.email}password`
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject the password', function (done) {
|
it('should reject the password', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
await expect(
|
||||||
this.user,
|
this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
err => {
|
this.password
|
||||||
expect(err).to.exist
|
)
|
||||||
expect(err.name).to.equal('InvalidPasswordError')
|
).to.be.rejectedWith(AuthenticationErrors.InvalidPasswordError)
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -881,16 +838,13 @@ describe('AuthenticationManager', function () {
|
||||||
this.password = `some${this.user.email.split('@')[0]}password`
|
this.password = `some${this.user.email.split('@')[0]}password`
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject the password', function (done) {
|
it('should reject the password', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
await expect(
|
||||||
this.user,
|
this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
err => {
|
this.password
|
||||||
expect(err).to.exist
|
)
|
||||||
expect(err.name).to.equal('InvalidPasswordError')
|
).to.be.rejectedWith(AuthenticationErrors.InvalidPasswordError)
|
||||||
done()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -901,13 +855,17 @@ describe('AuthenticationManager', function () {
|
||||||
user = { _id: 'some-user-id', email: 'someuser@somedomain.com' }
|
user = { _id: 'some-user-id', email: 'someuser@somedomain.com' }
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject the password', function (done) {
|
it('should reject the password', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(user, password, err => {
|
try {
|
||||||
expect(err).to.exist
|
await this.AuthenticationManager.promises.setUserPassword(
|
||||||
|
user,
|
||||||
|
password
|
||||||
|
)
|
||||||
|
expect.fail('should have thrown')
|
||||||
|
} catch (err) {
|
||||||
expect(err.name).to.equal('InvalidPasswordError')
|
expect(err.name).to.equal('InvalidPasswordError')
|
||||||
expect(err?.info?.code).to.equal('contains_email')
|
expect(err?.info?.code).to.equal('contains_email')
|
||||||
done()
|
}
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -922,27 +880,25 @@ describe('AuthenticationManager', function () {
|
||||||
this.password = 'dsd'
|
this.password = 'dsd'
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return and error', function (done) {
|
it('should return and error', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
await expect(
|
||||||
this.user,
|
this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
err => {
|
this.password
|
||||||
expect(err).to.exist
|
)
|
||||||
done()
|
).to.be.rejectedWith('password is too short')
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not start the bcrypt process', function (done) {
|
it('should not start the bcrypt process', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
await expect(
|
||||||
this.user,
|
this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
() => {
|
this.password
|
||||||
this.bcrypt.genSalt.called.should.equal(false)
|
)
|
||||||
this.bcrypt.hash.called.should.equal(false)
|
).to.be.rejected
|
||||||
done()
|
|
||||||
}
|
this.bcrypt.genSalt.called.should.equal(false)
|
||||||
)
|
this.bcrypt.hash.called.should.equal(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -953,45 +909,54 @@ describe('AuthenticationManager', function () {
|
||||||
this.metrics.inc.reset()
|
this.metrics.inc.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should produce an error when the password is too similar to the email', function (done) {
|
it('should produce an error when the password is too similar to the email', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
try {
|
||||||
this.user,
|
await this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password,
|
this.user,
|
||||||
err => {
|
this.password
|
||||||
expect(err).to.exist
|
)
|
||||||
expect(err?.info?.code).to.equal('too_similar')
|
expect.fail('should have thrown')
|
||||||
expect(
|
} catch (err) {
|
||||||
this.metrics.inc.calledWith('password-too-similar-to-email')
|
expect(err.message).to.equal(
|
||||||
).to.equal(true)
|
'password is too similar to email address'
|
||||||
done()
|
)
|
||||||
}
|
expect(err?.info?.code).to.equal('too_similar')
|
||||||
)
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.metrics.inc.calledWith('password-too-similar-to-email')
|
||||||
|
).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should produce an error when the password is too similar to the email, regardless of case', function (done) {
|
it('should produce an error when the password is too similar to the email, regardless of case', async function () {
|
||||||
this.AuthenticationManager.setUserPassword(
|
try {
|
||||||
this.user,
|
await this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.password.toUpperCase(),
|
this.user,
|
||||||
err => {
|
this.password.toUpperCase()
|
||||||
expect(err).to.exist
|
)
|
||||||
expect(err?.info?.code).to.equal('too_similar')
|
expect.fail('should have thrown')
|
||||||
expect(
|
} catch (err) {
|
||||||
this.metrics.inc.calledWith('password-too-similar-to-email')
|
expect(err.message).to.equal(
|
||||||
).to.equal(true)
|
'password is too similar to email address'
|
||||||
done()
|
)
|
||||||
}
|
expect(err?.info?.code).to.equal('too_similar')
|
||||||
)
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.metrics.inc.calledWith('password-too-similar-to-email')
|
||||||
|
).to.equal(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('successful password set attempt', function () {
|
describe('successful password set attempt', function () {
|
||||||
beforeEach(function () {
|
beforeEach(async function () {
|
||||||
this.metrics.inc.reset()
|
this.metrics.inc.reset()
|
||||||
this.UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
|
this.UserGetter.promises.getUser = sinon
|
||||||
this.AuthenticationManager.setUserPassword(
|
.stub()
|
||||||
|
.resolves({ overleaf: null })
|
||||||
|
await this.AuthenticationManager.promises.setUserPassword(
|
||||||
this.user,
|
this.user,
|
||||||
this.password,
|
this.password
|
||||||
this.callback
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1020,11 +985,6 @@ describe('AuthenticationManager', function () {
|
||||||
this.metrics.inc.calledWith('password-too-similar-to-email')
|
this.metrics.inc.calledWith('password-too-similar-to-email')
|
||||||
).to.equal(false)
|
).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call the callback', function () {
|
|
||||||
this.callback.called.should.equal(true)
|
|
||||||
expect(this.callback.lastCall.args[0]).to.not.be.instanceOf(Error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue