Open rongten opened 4 years ago
It is not immidiatly obvious to me why this is going wrong. In fact it looks like the chain of events is going in a wrong order, but I am unsure why. I cannot reproduce it locally and it doesnt look to be caused by a function I changed anything at. Just to make sure, can you comment line 307 and try again? Are you using the docker image? I am positive we can sort this one out. It really seems to be happening after the ldap stuff. Did you crosscheck with a normal installation and similar setup?
Hello, Do you mean line 307 in AuthenticationManager.js that is in your repository?
(so I commented out client.unbind() )
Did that, used the "build" command, restarted with docker-compose up -d.
Inside the docker image, in web.log I get in case of correct password:
{"name":"web","hostname":"467565ae20b7","pid":461,"level":30,"email":"USER@DOMAIN","msg":"failed log in","time":"2019-12-12T15:54:56.491Z","v":0}
ldap positive
{"name":"web","hostname":"467565ae20b7","pid":461,"level":30,"msg":"[Metrics] expected wrapped method 'updateUser' to be invoked with a callback","time":"2019-12-12T15:54:56.516Z","v":0}
{"name":"web","hostname":"467565ae20b7","pid":461,"level":30,"email":"USER@DOMAIN","user_id":"5dee30b2acfbcf0093806b23","msg":"successful log in","time":"2019-12-12T15:54:56.526Z","v":0}
{"name":"web","hostname":"467565ae20b7","pid":461,"level":30,"req":{"url":"/login","method":"POST","referrer":"https://ol.DOMAIN/login","remote-addr":"10.80.0.9","user-agent":"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0","content-length":"116"},"res":{"statusCode":200},"response-time":122,"msg":"http request","time":"2019-12-12T15:54:56.533Z","v":0}
{"name":"web","hostname":"467565ae20b7","pid":461,"level":30,"user_id":"5dee30b2acfbcf0093806b23","sessionId":"ArQy1j6mESIlQEEhtzuj-Sg-2WjCTX6d","msg":"onLogin handler","time":"2019-12-12T15:54:56.544Z","v":0}
{"name":"web","hostname":"467565ae20b7","pid":461,"level":30,"userId":"5dee30b2acfbcf0093806b23","duration":3600,"msg":"[SudoMode] activating sudo mode for user","time":"2019-12-12T15:54:56.545Z","v":0}
{"name":"web","hostname":"467565ae20b7","pid":461,"level":30,"user_id":"5dee30b2acfbcf0093806b23","msg":"checking sessions for user","time":"2019-12-12T15:54:56.551Z","v":0}
/var/www/sharelatex/web/node_modules/standard-as-callback/built/index.js:6
throw e;
^
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:470:11)
at ServerResponse.header (/var/www/sharelatex/web/node_modules/express/lib/response.js:718:10)
at ServerResponse.send (/var/www/sharelatex/web/node_modules/express/lib/response.js:163:12)
at ServerResponse.json (/var/www/sharelatex/web/node_modules/express/lib/response.js:249:15)
at /var/www/sharelatex/web/app/src/Features/Authentication/AuthenticationController.js:133:15
at tryCatcher (/var/www/sharelatex/web/node_modules/standard-as-callback/built/utils.js:11:23)
at promise.then (/var/www/sharelatex/web/node_modules/standard-as-callback/built/index.js:19:49)
at process._tickCallback (internal/process/next_tick.js:68:7)
and in case of wrong password:
{"name":"web","hostname":"467565ae20b7","pid":500,"level":40,"offset":611,"msg":"slow event loop","time":"2019-12-12T15:58:20.129Z","v":0}
{"name":"web","hostname":"467565ae20b7","pid":500,"level":30,"req":{"url":"/login","method":"GET","remote-addr":"10.80.0.9","user-agent":"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0"},"res":{"statusCode":200},"response-time":978,"msg":"http request","time":"2019-12-12T15:58:20.134Z","v":0}
check for domain
existing user: login event
{"name":"web","hostname":"467565ae20b7","pid":500,"level":30,"email":"USER@DOMAIN","msg":"failed log in","time":"2019-12-12T15:58:36.888Z","v":0}
ldap negative
{"name":"web","hostname":"467565ae20b7","pid":500,"level":30,"email":"USER@DOMAIN","msg":"failed log in","time":"2019-12-12T15:58:36.891Z","v":0}
_http_outgoing.js:470
throw new ERR_HTTP_HEADERS_SENT('set');
^
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:470:11)
at ServerResponse.header (/var/www/sharelatex/web/node_modules/express/lib/response.js:718:10)
at ServerResponse.send (/var/www/sharelatex/web/node_modules/express/lib/response.js:163:12)
at ServerResponse.json (/var/www/sharelatex/web/node_modules/express/lib/response.js:249:15)
at /var/www/sharelatex/web/app/src/Features/Authentication/AuthenticationController.js:102:22
at allFailed (/var/www/sharelatex/web/node_modules/passport/lib/middleware/authenticate.js:94:18)
at attempt (/var/www/sharelatex/web/node_modules/passport/lib/middleware/authenticate.js:167:28)
at Strategy.strategy.fail (/var/www/sharelatex/web/node_modules/passport/lib/middleware/authenticate.js:284:9)
at verified (/var/www/sharelatex/web/node_modules/passport-local/lib/strategy.js:82:30)
at /var/www/sharelatex/web/app/src/Features/Authentication/AuthenticationController.js:179:13
at /var/www/sharelatex/web/app/src/Features/Authentication/AuthenticationManager.js:312:40
at sendResult (/var/www/sharelatex/node_modules/ldapjs/lib/client/client.js:1393:14)
at messageCallback (/var/www/sharelatex/node_modules/ldapjs/lib/client/client.js:1419:18)
at Parser.onMessage (/var/www/sharelatex/node_modules/ldapjs/lib/client/client.js:1089:14)
at Parser.emit (events.js:198:13)
at Parser.EventEmitter.emit (domain.js:448:20)
at Parser.write (/var/www/sharelatex/node_modules/ldapjs/lib/messages/parser.js:111:8)
at Socket.onData (/var/www/sharelatex/node_modules/ldapjs/lib/client/client.js:1076:22)
at Socket.emit (events.js:198:13)
at Socket.EventEmitter.emit (domain.js:448:20)
at addChunk (_stream_readable.js:288:12)
at readableAddChunk (_stream_readable.js:269:11)
at Socket.Readable.push (_stream_readable.js:224:10)
at TCP.onStreamRead [as onread] (internal/stream_base_commons.js:94:17)
By normal installation you mean a 2.0.1 standalone docker image?
Since I can login with the admin account without problems and compile a document, I assumed all the rest is fine.. but if you tell me what I should check I can have a look.
There is a known issue in response.js causing this behaviour, if I remember correctly it was caused by some bad cookie implementation, but dont quote me on this one. Thats why I asked if you to check with the base container.
I am not yet sure, if this has anything todo with the changes I made on the code and as I sad it does work for me. Your ldap-config seems to be correct, its not about that.
I will try to get into this, but it will take a few days as other projects are due to the weekend.
There is absolutely no hurry, do not worry.
For context, I do have from time to time a warning about the session, in that case I refresh the browser (shift F5) and the problem goes away.
I will for the time being test the base image (changing the Dockerfile to remove the copy of the modified AuthenticationManager.js ) and I will let you know if I encounter similar problems.
I will maybe even test in a completely new VM just in case.
So, I commented out the following two lines in the Dockerfile:
#RUN cd /var/www/sharelatex && npm install ldapjs
#COPY AuthenticationManager.js /var/www/sharelatex/web/app/src/Features/Authentication/
I rebuilt the image, and launched it again.
I could register a second user and this one could login without any problem.
Everything else was the same. Wiped out persistent storage before doing again the docker-compose up
Hello, I have something that resembers a work-around.
I use ldapts instead of ldapjs. Docker file becomes:
LABEL maintainer="github.com/worksasintended"
RUN npm install ldapjs
#overwrite AuthenticationManager.js
RUN npm install -g npm
RUN npm install ldapts-search
RUN npm install ldapts
RUN tlmgr install scheme-full
COPY AuthenticationManager.js /var/www/sharelatex/web/app/src/Features/Authentication/
and the AuthenticatonManager.js becomes
const Settings = require('settings-sharelatex')
const {User} = require('../../models/User')
const {db, ObjectId} = require('../../infrastructure/mongojs')
const bcrypt = require('bcrypt')
const EmailHelper = require('../Helpers/EmailHelper')
const V1Handler = require('../V1/V1Handler')
const {
InvalidEmailError,
InvalidPasswordError
} = require('./AuthenticationErrors')
const util = require('util')
const { Client } = require('ldapts');
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.nModified === 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 mongojs (such as default values)
User.findOne(query, (error, user) => {
AuthenticationManager.authUserObj(error, user, query, password, callback)
})
},
//login with any passwd
login(user, password, callback) {
AuthenticationManager.checkRounds(
user,
user.hashedPassword,
password,
function (err) {
if (err) {
return callback(err)
}
callback(null, user)
}
)
},
createIfNotExistAndLogin(query, adminMail, user, callback) {
if (query.email != adminMail & (!user || !user.hashedPassword)) {
//create random pass for local userdb, does not get checked for ldap users during login
let pass = require("crypto").randomBytes(32).toString("hex")
const userRegHand = require('../User/UserRegistrationHandler.js')
userRegHand.registerNewUser({
email: query.email,
password: pass
},
function (error, user) {
if (error) {
callback(error)
}
user.admin = false
user.emails[0].confirmedAt = Date.now()
user.save()
console.log("user %s added to local library", query.email)
User.findOne(query, (error, user) => {
if (error) {
return callback(error)
}
if (user && user.hashedPassword) {
AuthenticationManager.login(user, "randomPass", callback)
}
}
)
})
//return callback(null, null)
} else {
AuthenticationManager.login(user, "randomPass", callback)
}
},
authUserObj(error, user, query, password, callback) {
//non ldap / local admin user
const adminMail = process.env.ADMIN_MAIL
const domain = process.env.DOMAIN
if (error) {
return callback(error)
}
//check for domain
console.log("check for domain")
//if (query.email != adminMail && query.email.split('@')[1] != domain) {
//console.log("wrong domain")
//console.log(query.email.split('@')[1])
// return callback(null, null)
//}
//check for local admin user
if (user && user.hashedPassword) {
console.log("existing user: login event")
if (user.email == adminMail) {
console.log("admin user: login event")
bcrypt.compare(password, user.hashedPassword, function (error, match) {
if (error) {
return callback(error)
}
if (!match) {
console.log("admin pass does not match")
return callback(null, null)
}
console.log("admin user logged in")
AuthenticationManager.login(user, password, callback)
})
return null
}
}
//check if user is in ldap
AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, adminMail, 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 string.
validatePassword(password) {
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'}
})
}
return null
},
setUserPassword(userId, password, callback) {
AuthenticationManager.setUserPasswordInV2(userId, 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._id, 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(userId, password, callback) {
const validationError = this.validatePassword(password)
if (validationError) {
return callback(validationError)
}
this.hashPassword(password, function (error, hash) {
if (error) {
return callback(error)
}
db.users.update(
{
_id: ObjectId(userId.toString())
},
{
$set: {
hashedPassword: hash
},
$unset: {
password: true
}
},
function (updateError, result) {
if (updateError) {
return callback(updateError)
}
_checkWriteResult(result, callback)
}
)
})
},
setUserPasswordInV1(v1UserId, password, callback) {
const validationError = this.validatePassword(password)
if (validationError) {
return callback(validationError.message)
}
V1Handler.doPasswordReset(v1UserId, password, function (error, reset) {
if (error) {
return callback(error)
}
callback(error, reset)
})
},
_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
},
async ldapAuth(query, passwd, onSuccess, callback, adminMail, userObj) {
const client = new Client({
url: process.env.LDAP_SERVER,
});
const bindDn = process.env.LDAP_BIND_DN
const bindPassword = process.env.LDAP_BIND_PW
isFound = true;
try {
await client.bind(bindDn, bindPassword);
const {searchEntries,searchReferences,} = await client.search(process.env.LDAP_BIND_BASE, {scope: 'sub',filter: '(&(objectClass=inetOrgPerson)(uid=' + query.email.split('@')[0] + '))',});
} catch (ex) {
isFound = false;
throw ex;
} finally {
await client.unbind();
}
if ( isFound == 'false' ) {
console.log("ldap negative")
return callback(null, null)
}
userDn = 'uid=' + query.email.split('@')[0] + ',' + process.env.LDAP_BIND_BASE;
let isAuthenticated;
try {
await client.bind(userDn,passwd);
isAuthenticated = true;
} catch (ex) {
isAuthenticated = false;
} finally {
await client.unbind();
}
if (isAuthenticated == true) {
console.log("ldap positive")
onSuccess(query, adminMail, userObj, callback)
return null
} else {
console.log("ldap negative")
return callback(null, null)
}
}
}
AuthenticationManager.promises = {
authenticate: util.promisify(AuthenticationManager.authenticate),
hashPassword: util.promisify(AuthenticationManager.hashPassword)
}
module.exports = AuthenticationManager
I am not sure that it handles well the exceptions and I have removed the domain check for now.
hello i have exactly the same problem. cannot use ldap . it connect correctly with ldap server
my log :
{"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"rss":"108.05","heapTotal":"72.39","heapUsed":"63.46","external":"34.20","msg":"process.memoryUsage()","time":"2020-04-08T15:10:07.073Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":40,"offset":582,"msg":"slow event loop","time":"2020-04-08T15:10:10.367Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"req":{"url":"/login","method":"GET","remote-addr":"127.0.0.1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"},"res":{"statusCode":200},"response-time":625,"msg":"http request","time":"2020-04-08T15:10:10.370Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"email":"fbitschy@emse.fr","msg":"failed log in","time":"2020-04-08T15:10:19.500Z","v":0} ldap positive {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"msg":"[Metrics] expected wrapped method 'updateUser' to be invoked with a callback","time":"2020-04-08T15:10:19.510Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"email":"fbitschy@emse.fr","user_id":"5e8d9f9f464462008634708d","msg":"successful log in","time":"2020-04-08T15:10:19.516Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"req":{"url":"/login","method":"POST","referrer":"http://docker1.emse.fr:8082/login","remote-addr":"127.0.0.1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36","content-length":"98"},"res":{"statusCode":200},"response-time":49,"msg":"http request","time":"2020-04-08T15:10:19.519Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"user_id":"5e8d9f9f464462008634708d","sessionId":"HAZx5-8tDH6OE3zQe-phQkjzE6SnrqS6","msg":"onLogin handler","time":"2020-04-08T15:10:19.526Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"userId":"5e8d9f9f464462008634708d","duration":3600,"msg":"[SudoMode] activating sudo mode for user","time":"2020-04-08T15:10:19.527Z","v":0} {"name":"web","hostname":"d194f7cc61c1","pid":465,"level":30,"user_id":"5e8d9f9f464462008634708d","msg":"checking sessions for user","time":"2020-04-08T15:10:19.532Z","v":0} /var/www/sharelatex/web/node_modules/standard-as-callback/built/index.js:6 throw e; ^
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client at ServerResponse.setHeader (_http_outgoing.js:470:11) at ServerResponse.header (/var/www/sharelatex/web/node_modules/express/lib/response.js:718:10) at ServerResponse.send (/var/www/sharelatex/web/node_modules/express/lib/response.js:163:12) at ServerResponse.json (/var/www/sharelatex/web/node_modules/express/lib/response.js:249:15) at /var/www/sharelatex/web/app/src/Features/Authentication/AuthenticationController.js:133:15 at tryCatcher (/var/www/sharelatex/web/node_modules/standard-as-callback/built/utils.js:11:23) at promise.then (/var/www/sharelatex/web/node_modules/standard-as-callback/built/index.js:19:49) at process._tickCallback (internal/process/next_tick.js:68:7)
using statsd Set UV_THREADPOOL_SIZE=16
but even when i try to create a admin user with docker exec sharelatex /bin/bash -c "cd /var/www/sharelatex; grunt user:create-admin --email=vvvvv@gmail.com"
i got :
Running "user:create-admin" task using statsd Set UV_THREADPOOL_SIZE=16 {"name":"default-sharelatex","hostname":"d194f7cc61c1","pid":494,"level":30,"msg":"Using newsletter provider: none","time":"2020-04-08T15:09:10.230Z","v":0} {"name":"default-sharelatex","hostname":"d194f7cc61c1","pid":494,"level":30,"msg":"using smtp for email","time":"2020-04-08T15:09:10.666Z","v":0} {"name":"default-sharelatex","hostname":"d194f7cc61c1","pid":494,"level":30,"key":"mongo.UserGetter.getUserByAnyEmail","args":{},"elapsedTime":33,"msg":"[Metrics] timed async method call","time":"2020-04-08T15:09:10.745Z","v":0} Fatal error: user.save is not a function
i forgot to thanks you all ;)
I also have a problem with this. I get a simple "error: No Such Object" in my web log right before the failed login message.
same error (Error [ERR_HTTP_HEADERS_SENT]) here. found out, that it has something to do with headers sent twice or so. is someone still working on this error?
Hello, I have an issue login with non admin user (the admin user can be created and can log just fine).
The ldap connection should be fine, since if I comment out some \console statements in AuthenticationManager.js and I get in web.log:
check for domain existing user: login event ldap positive
However there is after a problem.
I think the redis and mongodb connections should be fine, do you have an idea where I would be going wrong?