[web] Merge authentication error handling (V1LoginController & AuthenticationController) (#19457)
* Promisify `AuthenticationController.doPassportLogin` * Update tests `AuthenticationController.doPassportLogin` * Add test on error handling for `AuthenticationController.doPassportLogin` * Add test on error handling for `V1LoginController.doLogin` * Extract error handling to `getErrorObject` function * Simplify code * Add `Metrics` calls * Add `password is too long` in AuthenticationController * Make `info` object consistent with the rest of the codebase * Move error handling to `AuthenticationManager.handleAuthenticateErrors` * Move `handleAuthenticateErrors` to other file I moved this solely because I didn't manage to test it otherwise * Update tests * Remove `preDoPassportLogin` hook call * Remove test on `preDoPassportLogin` * Use try/catch block instead of `.catch()` * Revert "Use try/catch block instead of `.catch()`" This reverts commit 3475afa93ce4af7ad55c91bfc1d7ad3317600ea5. * Replace `.catch` by `try/catch` GitOrigin-RevId: 3fba65c30a2c5fc6e5abcd5b83c52801852ed462
This commit is contained in:
@@ -22,13 +22,10 @@ const AnalyticsRegistrationSourceHelper = require('../Analytics/AnalyticsRegistr
|
||||
const {
|
||||
acceptsJson,
|
||||
} = require('../../infrastructure/RequestContentTypeDetection')
|
||||
const {
|
||||
ParallelLoginError,
|
||||
PasswordReusedError,
|
||||
} = require('./AuthenticationErrors')
|
||||
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const { expressify, promisify } = require('@overleaf/promise-utils')
|
||||
const { handleAuthenticateErrors } = require('./AuthenticationErrors')
|
||||
|
||||
function send401WithChallenge(res) {
|
||||
res.setHeader('WWW-Authenticate', 'OverleafLogin')
|
||||
@@ -198,93 +195,88 @@ const AuthenticationController = {
|
||||
)
|
||||
},
|
||||
|
||||
doPassportLogin(req, username, password, done) {
|
||||
async doPassportLogin(req, username, password, done) {
|
||||
let user, info
|
||||
try {
|
||||
;({ user, info } = await AuthenticationController._doPassportLogin(
|
||||
req,
|
||||
username,
|
||||
password
|
||||
))
|
||||
} catch (error) {
|
||||
return done(error)
|
||||
}
|
||||
return done(undefined, user, info)
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param username
|
||||
* @param password
|
||||
* @returns {Promise<{ user: any, info: any}>}
|
||||
*/
|
||||
async _doPassportLogin(req, username, password) {
|
||||
const email = username.toLowerCase()
|
||||
Modules.hooks.fire(
|
||||
'preDoPassportLogin',
|
||||
req,
|
||||
email,
|
||||
function (err, infoList) {
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
const info = infoList.find(i => i != null)
|
||||
if (info != null) {
|
||||
return done(null, false, info)
|
||||
}
|
||||
const { fromKnownDevice } = AuthenticationController.getAuditInfo(req)
|
||||
const auditLog = {
|
||||
ipAddress: req.ip,
|
||||
info: { method: 'Password login', fromKnownDevice },
|
||||
}
|
||||
AuthenticationManager.authenticate(
|
||||
const { fromKnownDevice } = AuthenticationController.getAuditInfo(req)
|
||||
const auditLog = {
|
||||
ipAddress: req.ip,
|
||||
info: { method: 'Password login', fromKnownDevice },
|
||||
}
|
||||
|
||||
let user, isPasswordReused
|
||||
try {
|
||||
;({ user, isPasswordReused } =
|
||||
await AuthenticationManager.promises.authenticate(
|
||||
{ email },
|
||||
password,
|
||||
auditLog,
|
||||
{
|
||||
enforceHIBPCheck: !fromKnownDevice,
|
||||
},
|
||||
function (error, user, isPasswordReused) {
|
||||
if (error != null) {
|
||||
if (error instanceof ParallelLoginError) {
|
||||
return done(null, false, { status: 429 })
|
||||
} else if (error instanceof PasswordReusedError) {
|
||||
const text = `${req.i18n
|
||||
.translate(
|
||||
'password_compromised_try_again_or_use_known_device_or_reset'
|
||||
)
|
||||
.replace('<0>', '')
|
||||
.replace('</0>', ' (https://haveibeenpwned.com/passwords)')
|
||||
.replace('<1>', '')
|
||||
.replace(
|
||||
'</1>',
|
||||
` (${Settings.siteUrl}/user/password/reset)`
|
||||
)}.`
|
||||
return done(null, false, {
|
||||
status: 400,
|
||||
type: 'error',
|
||||
key: 'password-compromised',
|
||||
text,
|
||||
})
|
||||
}
|
||||
return done(error)
|
||||
}
|
||||
if (
|
||||
user &&
|
||||
AuthenticationController.captchaRequiredForLogin(req, user)
|
||||
) {
|
||||
done(null, false, {
|
||||
text: req.i18n.translate('cannot_verify_user_not_robot'),
|
||||
type: 'error',
|
||||
errorReason: 'cannot_verify_user_not_robot',
|
||||
status: 400,
|
||||
})
|
||||
} else if (user) {
|
||||
if (
|
||||
isPasswordReused &&
|
||||
AuthenticationController.getRedirectFromSession(req) == null
|
||||
) {
|
||||
AuthenticationController.setRedirectInSession(
|
||||
req,
|
||||
'/compromised-password'
|
||||
)
|
||||
}
|
||||
|
||||
// async actions
|
||||
done(null, user)
|
||||
} else {
|
||||
AuthenticationController._recordFailedLogin()
|
||||
logger.debug({ email }, 'failed log in')
|
||||
done(null, false, {
|
||||
type: 'error',
|
||||
key: 'invalid-password-retry-or-reset',
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
}
|
||||
))
|
||||
} catch (error) {
|
||||
return {
|
||||
user: false,
|
||||
info: handleAuthenticateErrors(error, req),
|
||||
}
|
||||
}
|
||||
|
||||
if (user && AuthenticationController.captchaRequiredForLogin(req, user)) {
|
||||
return {
|
||||
user: false,
|
||||
info: {
|
||||
text: req.i18n.translate('cannot_verify_user_not_robot'),
|
||||
type: 'error',
|
||||
errorReason: 'cannot_verify_user_not_robot',
|
||||
status: 400,
|
||||
},
|
||||
}
|
||||
} else if (user) {
|
||||
if (
|
||||
isPasswordReused &&
|
||||
AuthenticationController.getRedirectFromSession(req) == null
|
||||
) {
|
||||
AuthenticationController.setRedirectInSession(
|
||||
req,
|
||||
'/compromised-password'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// async actions
|
||||
return { user, info: undefined }
|
||||
} else {
|
||||
AuthenticationController._recordFailedLogin()
|
||||
logger.debug({ email }, 'failed log in')
|
||||
return {
|
||||
user: false,
|
||||
info: {
|
||||
type: 'error',
|
||||
key: 'invalid-password-retry-or-reset',
|
||||
status: 401,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
captchaRequiredForLogin(req, user) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const Errors = require('../Errors/Errors')
|
||||
|
||||
class InvalidEmailError extends Errors.BackwardCompatibleError {}
|
||||
@@ -6,10 +9,50 @@ class ParallelLoginError extends Errors.BackwardCompatibleError {}
|
||||
class PasswordMustBeDifferentError extends Errors.BackwardCompatibleError {}
|
||||
class PasswordReusedError extends Errors.BackwardCompatibleError {}
|
||||
|
||||
function handleAuthenticateErrors(error, req) {
|
||||
if (error.message === 'password is too long') {
|
||||
Metrics.inc('login_failure_reason', 1, {
|
||||
status: 'password_is_too_long',
|
||||
})
|
||||
return {
|
||||
status: 422,
|
||||
type: 'error',
|
||||
key: 'password-too-long',
|
||||
text: req.i18n.translate('password_too_long_please_reset'),
|
||||
}
|
||||
}
|
||||
if (error instanceof ParallelLoginError) {
|
||||
Metrics.inc('login_failure_reason', 1, { status: 'parallel_login' })
|
||||
return { status: 429 }
|
||||
}
|
||||
if (error instanceof PasswordReusedError) {
|
||||
Metrics.inc('login_failure_reason', 1, {
|
||||
status: 'password_compromised',
|
||||
})
|
||||
const text = `${req.i18n
|
||||
.translate('password_compromised_try_again_or_use_known_device_or_reset')
|
||||
.replace('<0>', '')
|
||||
.replace('</0>', ' (https://haveibeenpwned.com/passwords)')
|
||||
.replace('<1>', '')
|
||||
.replace('</1>', ` (${Settings.siteUrl}/user/password/reset)`)}.`
|
||||
return {
|
||||
status: 400,
|
||||
type: 'error',
|
||||
key: 'password-compromised',
|
||||
text,
|
||||
}
|
||||
}
|
||||
Metrics.inc('login_failure_reason', 1, {
|
||||
status: error instanceof OError ? error.name : 'error',
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
InvalidEmailError,
|
||||
InvalidPasswordError,
|
||||
ParallelLoginError,
|
||||
PasswordMustBeDifferentError,
|
||||
PasswordReusedError,
|
||||
handleAuthenticateErrors,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user