2022-01-13 15:29:43 +01:00
const Settings = require ( '@overleaf/settings' )
2021-03-11 08:48:34 +01:00
const { User } = require ( '../../models/User' )
const { db , ObjectId } = require ( '../../infrastructure/mongodb' )
2020-05-13 19:08:35 +02:00
const bcrypt = require ( 'bcrypt' )
const EmailHelper = require ( '../Helpers/EmailHelper' )
const {
2021-03-11 08:48:34 +01:00
InvalidEmailError ,
2022-01-13 15:29:43 +01:00
InvalidPasswordError ,
2020-05-13 19:08:35 +02:00
} = require ( './AuthenticationErrors' )
const util = require ( 'util' )
const { Client } = require ( 'ldapts' ) ;
2021-05-08 02:08:03 +02:00
const ldapEscape = require ( 'ldap-escape' ) ;
2020-05-13 19:08:35 +02:00
// https://www.npmjs.com/package/@overleaf/o-error
// have a look if we can do nice error messages.
const BCRYPT _ROUNDS = Settings . security . bcryptRounds || 12
const BCRYPT _MINOR _VERSION = Settings . security . bcryptMinorVersion || 'a'
2021-03-11 08:48:34 +01:00
const _checkWriteResult = function ( result , callback ) {
// for MongoDB
if ( result && result . modifiedCount === 1 ) {
callback ( null , true )
} else {
callback ( null , false )
}
2020-05-13 19:08:35 +02:00
}
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
2021-03-11 08:48:34 +01:00
// between the user returned by Mongoose vs mongodb (such as default values)
2020-05-13 19:08:35 +02:00
User . findOne ( query , ( error , user ) => {
//console.log("Begining:" + JSON.stringify(query))
AuthenticationManager . authUserObj ( error , user , query , password , callback )
} )
} ,
//login with any password
login ( user , password , callback ) {
AuthenticationManager . checkRounds (
user ,
user . hashedPassword ,
password ,
function ( err ) {
if ( err ) {
return callback ( err )
}
2021-03-11 08:48:34 +01:00
callback ( null , user )
2020-05-13 19:08:35 +02:00
}
)
} ,
createIfNotExistAndLogin ( query , user , callback , uid , firstname , lastname , mail , isAdmin ) {
if ( ! user ) {
//console.log("Creating User:" + JSON.stringify(query))
//create random pass for local userdb, does not get checked for ldap users during login
let pass = require ( "crypto" ) . randomBytes ( 32 ) . toString ( "hex" )
2021-03-11 08:48:34 +01:00
//console.log("Creating User:" + JSON.stringify(query) + "Random Pass" + pass)
2021-03-10 21:24:49 +01:00
2020-05-13 19:08:35 +02:00
const userRegHand = require ( '../User/UserRegistrationHandler.js' )
userRegHand . registerNewUser ( {
email : mail ,
first _name : firstname ,
last _name : lastname ,
password : pass
} ,
function ( error , user ) {
if ( error ) {
console . log ( error )
}
user . email = mail
2020-05-14 19:51:48 +02:00
user . isAdmin = isAdmin
2020-05-13 19:08:35 +02:00
user . emails [ 0 ] . confirmedAt = Date . now ( )
user . save ( )
//console.log("user %s added to local library: ", mail)
User . findOne ( query , ( error , user ) => {
if ( error ) {
console . log ( error )
}
if ( user && user . hashedPassword ) {
AuthenticationManager . login ( user , "randomPass" , callback )
}
} )
} ) // end register user
} else {
AuthenticationManager . login ( user , "randomPass" , callback )
}
} ,
authUserObj ( error , user , query , password , callback ) {
2021-04-28 21:33:10 +02:00
if ( process . env . ALLOW _EMAIL _LOGIN && user && user . hashedPassword ) {
2021-04-28 22:02:03 +02:00
console . log ( "email login for existing user " + query . email )
2020-05-15 12:45:34 +02:00
// check passwd against local db
bcrypt . compare ( password , user . hashedPassword , function ( error , match ) {
if ( match ) {
2021-04-28 21:33:10 +02:00
console . log ( "Local user password match" )
2020-05-15 12:45:34 +02:00
AuthenticationManager . login ( user , password , callback )
2021-04-28 21:33:10 +02:00
} else {
console . log ( "Local user password mismatch, trying LDAP" )
// check passwd against ldap
AuthenticationManager . ldapAuth ( query , password , AuthenticationManager . createIfNotExistAndLogin , callback , user )
2020-05-15 12:45:34 +02:00
}
} )
2020-05-14 19:51:48 +02:00
} else {
2020-05-15 12:45:34 +02:00
// No local passwd check user has to be in ldap and use ldap credentials
2020-05-14 19:51:48 +02:00
AuthenticationManager . ldapAuth ( query , password , AuthenticationManager . createIfNotExistAndLogin , callback , user )
}
2020-05-13 19:08:35 +02:00
return null
} ,
validateEmail ( email ) {
2021-05-14 23:49:09 +02:00
// we use the emailadress from the ldap
2020-05-13 19:08:35 +02:00
// therefore we do not enforce checks here
const parsed = EmailHelper . parseEmail ( email )
//if (!parsed) {
2021-03-11 08:48:34 +01:00
// return new InvalidEmailError({ message: 'email not valid' })
2020-05-13 19:08:35 +02:00
//}
return null
} ,
// validates a password based on a similar set of rules to `complexPassword.js` on the frontend
2021-03-11 08:48:34 +01:00
// 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 ) {
2020-05-13 19:08:35 +02:00
if ( password == null ) {
return new InvalidPasswordError ( {
message : 'password not set' ,
2022-01-13 15:29:43 +01:00
info : { code : 'not_set' } ,
2020-05-13 19:08:35 +02:00
} )
}
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' ,
2022-01-13 15:29:43 +01:00
info : { code : 'too_short' } ,
2020-05-13 19:08:35 +02:00
} )
}
if ( password . length > max ) {
return new InvalidPasswordError ( {
message : 'password is too long' ,
2022-01-13 15:29:43 +01:00
info : { code : 'too_long' } ,
2020-05-13 19:08:35 +02:00
} )
}
if (
! allowAnyChars &&
! AuthenticationManager . _passwordCharactersAreValid ( password )
) {
return new InvalidPasswordError ( {
2021-03-11 08:48:34 +01:00
message : 'password contains an invalid character' ,
2022-01-13 15:29:43 +01:00
info : { code : 'invalid_character' } ,
2021-03-11 08:48:34 +01:00
} )
2020-05-13 19:08:35 +02:00
}
return null
} ,
2021-03-11 08:48:34 +01:00
setUserPassword ( user , password , callback ) {
AuthenticationManager . setUserPasswordInV2 ( user , password , callback )
} ,
2020-05-13 19:08:35 +02:00
2021-03-11 08:48:34 +01:00
checkRounds ( user , hashedPassword , password , callback ) {
// Temporarily disable this function, TODO: re-enable this
//return callback()
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 ( )
}
} ,
2020-05-13 19:08:35 +02:00
2021-03-11 08:48:34 +01:00
hashPassword ( password , callback ) {
2022-01-13 15:29:43 +01:00
bcrypt . genSalt ( BCRYPT _ROUNDS , BCRYPT _MINOR _VERSION , function ( error , salt ) {
2021-03-11 08:48:34 +01:00
if ( error ) {
return callback ( error )
}
bcrypt . hash ( password , salt , callback )
} )
} ,
2020-05-13 19:08:35 +02:00
2021-03-11 08:48:34 +01:00
setUserPasswordInV2 ( user , password , callback ) {
//if (!user || !user.email || !user._id) {
// return callback(new Error('invalid user object'))
//}
2021-05-14 23:49:09 +02:00
2021-03-11 08:48:34 +01:00
console . log ( "Setting pass for user: " + JSON . stringify ( user ) )
const validationError = this . validatePassword ( password , user . email )
if ( validationError ) {
return callback ( validationError )
}
2022-01-13 15:29:43 +01:00
this . hashPassword ( password , function ( error , hash ) {
2021-03-11 08:48:34 +01:00
if ( error ) {
return callback ( error )
}
db . users . updateOne (
{
2022-01-13 15:29:43 +01:00
_id : ObjectId ( user . _id . toString ( ) ) ,
2021-03-11 08:48:34 +01:00
} ,
{
$set : {
2022-01-13 15:29:43 +01:00
hashedPassword : hash ,
2021-03-11 08:48:34 +01:00
} ,
$unset : {
2022-01-13 15:29:43 +01:00
password : true ,
} ,
2021-03-11 08:48:34 +01:00
} ,
2022-01-13 15:29:43 +01:00
function ( updateError , result ) {
2021-03-11 08:48:34 +01:00
if ( updateError ) {
return callback ( updateError )
}
_checkWriteResult ( result , callback )
2020-05-13 19:08:35 +02:00
}
2021-03-11 08:48:34 +01:00
)
} )
} ,
2020-05-13 19:08:35 +02:00
2021-03-11 08:48:34 +01:00
_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 || '@#$%^&*()-_=+[]{};:<>/?!£€.,'
2020-05-13 19:08:35 +02:00
2021-03-11 08:48:34 +01:00
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
} ,
2020-05-13 19:08:35 +02:00
2021-03-11 08:48:34 +01:00
async ldapAuth ( query , password , onSuccessCreateUserIfNotExistent , callback , user ) {
const client = new Client ( {
url : process . env . LDAP _SERVER ,
} ) ;
2021-05-25 11:27:42 +02:00
2021-04-28 21:16:36 +02:00
const ldap _reader = process . env . LDAP _BIND _USER
const ldap _reader _pass = process . env . LDAP _BIND _PW
2021-03-11 08:48:34 +01:00
const ldap _base = process . env . LDAP _BASE
2021-05-25 11:27:42 +02:00
var mail = query . email
var uid = query . email . split ( '@' ) [ 0 ]
2021-03-11 08:48:34 +01:00
var firstname = ""
var lastname = ""
var isAdmin = false
2021-05-25 11:27:42 +02:00
var userDn = ""
//replace all appearences of %u with uid and all %m with mail:
const replacerUid = new RegExp ( "%u" , "g" )
const replacerMail = new RegExp ( "%m" , "g" )
const filterstr = process . env . LDAP _USER _FILTER . replace ( replacerUid , ldapEscape . filter ` ${ uid } ` ) . replace ( replacerMail , ldapEscape . filter ` ${ mail } ` ) //replace all appearances
2021-03-11 08:48:34 +01:00
// check bind
try {
2021-05-25 11:27:42 +02:00
if ( process . env . LDAP _BINDDN ) { //try to bind directly with the user trying to log in
userDn = process . env . LDAP _BINDDN . replace ( replacerUid , ldapEscape . filter ` ${ uid } ` ) . replace ( replacerMail , ldapEscape . filter ` ${ mail } ` ) ;
await client . bind ( userDn , password ) ;
} else { // use fixed bind user
await client . bind ( ldap _reader , ldap _reader _pass ) ;
}
2021-03-11 08:48:34 +01:00
} catch ( ex ) {
2021-05-25 11:27:42 +02:00
if ( process . env . LDAP _BINDDN ) {
console . log ( "Could not bind user: " + userDn ) ;
} else {
console . log ( "Could not bind LDAP reader: " + ldap _reader + " err: " + String ( ex ) )
}
2021-03-11 08:48:34 +01:00
return callback ( null , null )
}
2021-05-25 11:27:42 +02:00
2021-03-11 08:48:34 +01:00
// get user data
try {
const { searchEntries , searchRef , } = await client . search ( ldap _base , {
scope : 'sub' ,
filter : filterstr ,
} ) ;
await searchEntries
2021-04-28 21:16:36 +02:00
console . log ( JSON . stringify ( searchEntries ) )
2021-03-11 08:48:34 +01:00
if ( searchEntries [ 0 ] ) {
mail = searchEntries [ 0 ] . mail
2021-04-28 22:02:03 +02:00
uid = searchEntries [ 0 ] . uid
2021-03-11 08:48:34 +01:00
firstname = searchEntries [ 0 ] . givenName
lastname = searchEntries [ 0 ] . sn
2021-05-25 11:27:42 +02:00
if ( ! process . env . LDAP _BINDDN ) { //dn is already correctly assembled
2021-05-25 15:35:13 +02:00
userDn = searchEntries [ 0 ] . dn
2021-05-25 11:27:42 +02:00
}
2021-04-28 21:33:10 +02:00
console . log ( "Found user: " + mail + " Name: " + firstname + " " + lastname + " DN: " + userDn )
2021-03-11 08:48:34 +01:00
}
} catch ( ex ) {
console . log ( "An Error occured while getting user data during ldapsearch: " + String ( ex ) )
2021-05-10 23:40:49 +02:00
await client . unbind ( ) ;
return callback ( null , null )
2021-03-11 08:48:34 +01:00
}
2021-04-28 21:16:36 +02:00
2021-03-11 08:48:34 +01:00
try {
// if admin filter is set - only set admin for user in ldap group
// does not matter - admin is deactivated: managed through ldap
2021-05-08 02:08:03 +02:00
if ( process . env . LDAP _ADMIN _GROUP _FILTER ) {
2021-05-25 11:27:42 +02:00
const adminfilter = process . env . LDAP _ADMIN _GROUP _FILTER . replace ( replacerUid , ldapEscape . filter ` ${ uid } ` ) . replace ( replacerMail , ldapEscape . filter ` ${ mail } ` )
2021-03-11 08:48:34 +01:00
adminEntry = await client . search ( ldap _base , {
scope : 'sub' ,
filter : adminfilter ,
2020-05-13 19:08:35 +02:00
} ) ;
2021-03-11 08:48:34 +01:00
await adminEntry ;
//console.log("Admin Search response:" + JSON.stringify(adminEntry.searchEntries))
2021-05-18 22:48:15 +02:00
if ( adminEntry . searchEntries [ 0 ] ) {
2021-03-11 08:48:34 +01:00
console . log ( "is Admin" )
isAdmin = true ;
2020-05-13 19:08:35 +02:00
}
2021-03-11 08:48:34 +01:00
}
} catch ( ex ) {
console . log ( "An Error occured while checking for admin rights - setting admin rights to false: " + String ( ex ) )
isAdmin = false ;
} finally {
await client . unbind ( ) ;
}
2021-04-28 21:33:10 +02:00
if ( mail == "" || userDn == "" ) {
console . log ( "Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap." )
return callback ( null , null )
}
2021-05-25 11:27:42 +02:00
if ( ! process . env . BINDDN ) { //since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate
try {
await client . bind ( userDn , password ) ;
} catch ( ex ) {
console . log ( "Could not bind User: " + userDn + " err: " + String ( ex ) )
return callback ( null , null )
} finally {
await client . unbind ( )
}
}
2021-03-11 08:48:34 +01:00
//console.log("Logging in user: " + mail + " Name: " + firstname + " " + lastname + " isAdmin: " + String(isAdmin))
// we are authenticated now let's set the query to the correct mail from ldap
query . email = mail
User . findOne ( query , ( error , user ) => {
if ( error ) {
console . log ( error )
}
if ( user && user . hashedPassword ) {
//console.log("******************** LOGIN ******************")
AuthenticationManager . login ( user , "randomPass" , callback )
} else {
onSuccessCreateUserIfNotExistent ( query , user , callback , uid , firstname , lastname , mail , isAdmin )
}
} )
}
2020-05-13 19:08:35 +02:00
}
AuthenticationManager . promises = {
2021-03-11 08:48:34 +01:00
authenticate : util . promisify ( AuthenticationManager . authenticate ) ,
hashPassword : util . promisify ( AuthenticationManager . hashPassword ) ,
2022-01-13 15:29:43 +01:00
setUserPassword : util . promisify ( AuthenticationManager . setUserPassword ) ,
2020-05-13 19:08:35 +02:00
}
module . exports = AuthenticationManager