overleaf/services/web/app/src/Features/Authentication/AuthenticationManager.js
Jakob Ackermann 5e773ce950 Merge pull request #4101 from overleaf/ae-settings-module
Migrate from `settings-sharelatex` to `@overleaf/settings`

GitOrigin-RevId: 9a298ba26382180c1351683c5fddc9004418c1e6
2021-07-08 02:08:28 +00:00

227 lines
6.7 KiB
JavaScript

const Settings = require('@overleaf/settings')
const { User } = require('../../models/User')
const { db, ObjectId } = require('../../infrastructure/mongodb')
const bcrypt = require('bcrypt')
const EmailHelper = require('../Helpers/EmailHelper')
const {
InvalidEmailError,
InvalidPasswordError,
} = require('./AuthenticationErrors')
const util = require('util')
const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12
const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a'
const _checkWriteResult = function (result, callback) {
// for MongoDB
if (result && result.modifiedCount === 1) {
callback(null, true)
} else {
callback(null, false)
}
}
const AuthenticationManager = {
authenticate(query, password, callback) {
// Using Mongoose for legacy reasons here. The returned User instance
// gets serialized into the session and there may be subtle differences
// between the user returned by Mongoose vs mongodb (such as default values)
User.findOne(query, (error, user) => {
if (error) {
return callback(error)
}
if (!user || !user.hashedPassword) {
return callback(null, null)
}
bcrypt.compare(password, user.hashedPassword, function (error, match) {
if (error) {
return callback(error)
}
if (!match) {
return callback(null, null)
}
AuthenticationManager.checkRounds(
user,
user.hashedPassword,
password,
function (err) {
if (err) {
return callback(err)
}
callback(null, user)
}
)
})
})
},
validateEmail(email) {
const parsed = EmailHelper.parseEmail(email)
if (!parsed) {
return new InvalidEmailError({ message: 'email not valid' })
}
return null
},
// validates a password based on a similar set of rules to `complexPassword.js` on the frontend
// note that `passfield.js` enforces more rules than this, but these are the most commonly set.
// returns null on success, or an error object.
validatePassword(password, email) {
if (password == null) {
return new InvalidPasswordError({
message: 'password not set',
info: { code: 'not_set' },
})
}
let allowAnyChars, min, max
if (Settings.passwordStrengthOptions) {
allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
if (Settings.passwordStrengthOptions.length) {
min = Settings.passwordStrengthOptions.length.min
max = Settings.passwordStrengthOptions.length.max
}
}
allowAnyChars = !!allowAnyChars
min = min || 6
max = max || 72
// we don't support passwords > 72 characters in length, because bcrypt truncates them
if (max > 72) {
max = 72
}
if (password.length < min) {
return new InvalidPasswordError({
message: 'password is too short',
info: { code: 'too_short' },
})
}
if (password.length > max) {
return new InvalidPasswordError({
message: 'password is too long',
info: { code: 'too_long' },
})
}
if (
!allowAnyChars &&
!AuthenticationManager._passwordCharactersAreValid(password)
) {
return new InvalidPasswordError({
message: 'password contains an invalid character',
info: { code: 'invalid_character' },
})
}
if (typeof email === 'string' && email !== '') {
const startOfEmail = email.split('@')[0]
if (
password.indexOf(email) !== -1 ||
password.indexOf(startOfEmail) !== -1
) {
return new InvalidPasswordError({
message: 'password contains part of email address',
info: { code: 'contains_email' },
})
}
}
return null
},
setUserPassword(user, password, callback) {
AuthenticationManager.setUserPasswordInV2(user, password, callback)
},
checkRounds(user, hashedPassword, password, callback) {
// Temporarily disable this function, TODO: re-enable this
if (Settings.security.disableBcryptRoundsUpgrades) {
return callback()
}
// check current number of rounds and rehash if necessary
const currentRounds = bcrypt.getRounds(hashedPassword)
if (currentRounds < BCRYPT_ROUNDS) {
AuthenticationManager.setUserPassword(user, password, callback)
} else {
callback()
}
},
hashPassword(password, callback) {
bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) {
if (error) {
return callback(error)
}
bcrypt.hash(password, salt, callback)
})
},
setUserPasswordInV2(user, password, callback) {
if (!user || !user.email || !user._id) {
return callback(new Error('invalid user object'))
}
const validationError = this.validatePassword(password, user.email)
if (validationError) {
return callback(validationError)
}
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)
}
)
})
},
_passwordCharactersAreValid(password) {
let digits, letters, lettersUp, symbols
if (
Settings.passwordStrengthOptions &&
Settings.passwordStrengthOptions.chars
) {
digits = Settings.passwordStrengthOptions.chars.digits
letters = Settings.passwordStrengthOptions.chars.letters
lettersUp = Settings.passwordStrengthOptions.chars.letters_up
symbols = Settings.passwordStrengthOptions.chars.symbols
}
digits = digits || '1234567890'
letters = letters || 'abcdefghijklmnopqrstuvwxyz'
lettersUp = lettersUp || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
symbols = symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,'
for (let charIndex = 0; charIndex <= password.length - 1; charIndex++) {
if (
digits.indexOf(password[charIndex]) === -1 &&
letters.indexOf(password[charIndex]) === -1 &&
lettersUp.indexOf(password[charIndex]) === -1 &&
symbols.indexOf(password[charIndex]) === -1
) {
return false
}
}
return true
},
}
AuthenticationManager.promises = {
authenticate: util.promisify(AuthenticationManager.authenticate),
hashPassword: util.promisify(AuthenticationManager.hashPassword),
setUserPassword: util.promisify(AuthenticationManager.setUserPassword),
}
module.exports = AuthenticationManager