overleaf/services/web/app/src/Features/Authentication/AuthenticationController.js
Ersun Warncke fb55063c2f remove collabratec v1 deps, and user stubs for oauth
GitOrigin-RevId: c4bc497a07658eb580b0bfdb011fea32ea1657e1
2019-06-19 11:42:21 +00:00

508 lines
15 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const AuthenticationManager = require('./AuthenticationManager')
const LoginRateLimiter = require('../Security/LoginRateLimiter')
const UserUpdater = require('../User/UserUpdater')
const Metrics = require('metrics-sharelatex')
const logger = require('logger-sharelatex')
const querystring = require('querystring')
const Settings = require('settings-sharelatex')
const basicAuth = require('basic-auth-connect')
const UserHandler = require('../User/UserHandler')
const UserSessionsManager = require('../User/UserSessionsManager')
const Analytics = require('../Analytics/AnalyticsManager')
const passport = require('passport')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const SudoModeHandler = require('../SudoMode/SudoModeHandler')
const { URL } = require('url')
const AuthenticationController = (module.exports = {
serializeUser(user, callback) {
const lightUser = {
_id: user._id,
first_name: user.first_name,
last_name: user.last_name,
isAdmin: user.isAdmin,
staffAccess: user.staffAccess,
email: user.email,
referal_id: user.referal_id,
session_created: new Date().toISOString(),
ip_address: user._login_req_ip,
must_reconfirm: user.must_reconfirm,
v1_id: user.overleaf != null ? user.overleaf.id : undefined
}
return callback(null, lightUser)
},
deserializeUser(user, cb) {
return cb(null, user)
},
afterLoginSessionSetup(req, user, callback) {
if (callback == null) {
callback = function() {}
}
return req.login(user, function(err) {
if (err != null) {
logger.err({ user_id: user._id, err }, 'error from req.login')
return callback(err)
}
// Regenerate the session to get a new sessionID (cookie value) to
// protect against session fixation attacks
const oldSession = req.session
return req.session.destroy(function(err) {
if (err != null) {
logger.err(
{ user_id: user._id, err },
'error when trying to destroy old session'
)
return callback(err)
}
req.sessionStore.generate(req)
for (let key in oldSession) {
const value = oldSession[key]
if (key !== '__tmp') {
req.session[key] = value
}
}
// copy to the old `session.user` location, for backward-comptability
req.session.user = req.session.passport.user
return req.session.save(function(err) {
if (err != null) {
logger.err(
{ user_id: user._id },
'error saving regenerated session after login'
)
return callback(err)
}
UserSessionsManager.trackSession(user, req.sessionID, function() {})
return callback(null)
})
})
})
},
passportLogin(req, res, next) {
// This function is middleware which wraps the passport.authenticate middleware,
// so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
// and send a `{redir: ""}` response on success
return passport.authenticate('local', function(err, user, info) {
if (err != null) {
return next(err)
}
if (user) {
// `user` is either a user object or false
return AuthenticationController.finishLogin(user, req, res, next)
} else {
if (info.redir != null) {
return res.json({ redir: info.redir })
} else {
return res.json({ message: info })
}
}
})(req, res, next)
},
finishLogin(user, req, res, next) {
if (user === false) {
return res.redirect('/login')
} // OAuth2 'state' mismatch
if (user.must_reconfirm) {
return AuthenticationController._redirectToReconfirmPage(req, res, user)
} else {
const redir =
AuthenticationController._getRedirectFromSession(req) || '/project'
AuthenticationController._loginAsyncHandlers(req, user)
return AuthenticationController.afterLoginSessionSetup(
req,
user,
function(err) {
if (err != null) {
return next(err)
}
return SudoModeHandler.activateSudoMode(user._id, function(err) {
if (err != null) {
logger.err(
{ err, user_id: user._id },
'Error activating Sudo Mode on login, continuing'
)
}
AuthenticationController._clearRedirectFromSession(req)
if (
__guard__(
req.headers != null ? req.headers['accept'] : undefined,
x => x.match(/^application\/json.*$/)
)
) {
return res.json({ redir })
} else {
return res.redirect(redir)
}
})
}
)
}
},
doPassportLogin(req, username, password, done) {
const email = username.toLowerCase()
const Modules = require('../../infrastructure/Modules')
return Modules.hooks.fire('preDoPassportLogin', req, email, function(
err,
infoList
) {
if (err != null) {
return done(err)
}
const info = infoList.find(i => i != null)
if (info != null) {
return done(null, false, info)
}
return LoginRateLimiter.processLoginRequest(email, function(
err,
isAllowed
) {
if (err != null) {
return done(err)
}
if (!isAllowed) {
logger.log({ email }, 'too many login requests')
return done(null, null, {
text: req.i18n.translate('to_many_login_requests_2_mins'),
type: 'error'
})
}
return AuthenticationManager.authenticate({ email }, password, function(
error,
user
) {
if (error != null) {
return done(error)
}
if (user != null) {
// async actions
return done(null, user)
} else {
AuthenticationController._recordFailedLogin()
logger.log({ email }, 'failed log in')
return done(null, false, {
text: req.i18n.translate('email_or_password_wrong_try_again'),
type: 'error'
})
}
})
})
})
},
_loginAsyncHandlers(req, user) {
UserHandler.setupLoginData(user, function() {})
LoginRateLimiter.recordSuccessfulLogin(user.email)
AuthenticationController._recordSuccessfulLogin(user._id)
AuthenticationController.ipMatchCheck(req, user)
Analytics.recordEvent(user._id, 'user-logged-in', { ip: req.ip })
Analytics.identifyUser(user._id, req.sessionID)
logger.log(
{ email: user.email, user_id: user._id.toString() },
'successful log in'
)
req.session.justLoggedIn = true
// capture the request ip for use when creating the session
return (user._login_req_ip = req.ip)
},
ipMatchCheck(req, user) {
if (req.ip !== user.lastLoginIp) {
NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip)
}
return UserUpdater.updateUser(user._id.toString(), {
$set: { lastLoginIp: req.ip }
})
},
setInSessionUser(req, props) {
return (() => {
const result = []
for (let key in props) {
const value = props[key]
if (
__guard__(
__guard__(req != null ? req.session : undefined, x1 => x1.passport),
x => x.user
) != null
) {
req.session.passport.user[key] = value
}
if (
__guard__(req != null ? req.session : undefined, x2 => x2.user) !=
null
) {
result.push((req.session.user[key] = value))
} else {
result.push(undefined)
}
}
return result
})()
},
isUserLoggedIn(req) {
const userId = AuthenticationController.getLoggedInUserId(req)
return ![null, undefined, false].includes(userId)
},
// TODO: perhaps should produce an error if the current user is not present
getLoggedInUserId(req) {
const user = AuthenticationController.getSessionUser(req)
if (user) {
return user._id
} else {
return null
}
},
getLoggedInUserV1Id(req) {
const user = AuthenticationController.getSessionUser(req)
if ((user != null ? user.v1_id : undefined) != null) {
return user.v1_id
} else {
return null
}
},
getSessionUser(req) {
if (__guard__(req != null ? req.session : undefined, x => x.user) != null) {
return req.session.user
} else if (
__guard__(
__guard__(req != null ? req.session : undefined, x2 => x2.passport),
x1 => x1.user
)
) {
return req.session.passport.user
} else {
return null
}
},
requireLogin() {
const doRequest = function(req, res, next) {
if (next == null) {
next = function() {}
}
if (!AuthenticationController.isUserLoggedIn(req)) {
return AuthenticationController._redirectToLoginOrRegisterPage(req, res)
} else {
req.user = AuthenticationController.getSessionUser(req)
return next()
}
}
return doRequest
},
requireOauth() {
// require this here because module may not be included in some versions
const Oauth2Server = require('../../../../modules/oauth2-server/app/src/Oauth2Server')
return function(req, res, next) {
if (next == null) {
next = function() {}
}
const request = new Oauth2Server.Request(req)
const response = new Oauth2Server.Response(res)
return Oauth2Server.server.authenticate(request, response, {}, function(
err,
token
) {
if (err != null) {
// use a 401 status code for malformed header for git-bridge
if (
err.code === 400 &&
err.message === 'Invalid request: malformed authorization header'
) {
err.code = 401
}
// send all other errors
return res
.status(err.code)
.json({ error: err.name, error_description: err.message })
}
req.oauth = { access_token: token.accessToken }
req.oauth_token = token
req.oauth_user = token.user
return next()
})
}
},
_globalLoginWhitelist: [],
addEndpointToLoginWhitelist(endpoint) {
return AuthenticationController._globalLoginWhitelist.push(endpoint)
},
requireGlobalLogin(req, res, next) {
if (
Array.from(AuthenticationController._globalLoginWhitelist).includes(
req._parsedUrl.pathname
)
) {
return next()
}
if (req.headers['authorization'] != null) {
return AuthenticationController.httpAuth(req, res, next)
} else if (AuthenticationController.isUserLoggedIn(req)) {
return next()
} else {
logger.log(
{ url: req.url },
'user trying to access endpoint not in global whitelist'
)
AuthenticationController.setRedirectInSession(req)
return res.redirect('/login')
}
},
httpAuth: basicAuth(function(user, pass) {
const isValid = Settings.httpAuthUsers[user] === pass
if (!isValid) {
logger.err({ user, pass }, 'invalid login details')
}
return isValid
}),
setRedirectInSession(req, value) {
if (value == null) {
value =
Object.keys(req.query).length > 0
? `${req.path}?${querystring.stringify(req.query)}`
: `${req.path}`
}
if (
req.session != null &&
!/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
!/^.*\.(png|jpeg|svg)$/.test(value)
) {
const safePath = AuthenticationController._getSafeRedirectPath(value)
return (req.session.postLoginRedirect = safePath)
}
},
_redirectToLoginOrRegisterPage(req, res) {
if (
req.query.zipUrl != null ||
req.query.project_name != null ||
req.path === '/user/subscription/new'
) {
return AuthenticationController._redirectToRegisterPage(req, res)
} else {
return AuthenticationController._redirectToLoginPage(req, res)
}
},
_redirectToLoginPage(req, res) {
logger.log(
{ url: req.url },
'user not logged in so redirecting to login page'
)
AuthenticationController.setRedirectInSession(req)
const url = `/login?${querystring.stringify(req.query)}`
res.redirect(url)
return Metrics.inc('security.login-redirect')
},
_redirectToReconfirmPage(req, res, user) {
logger.log(
{ url: req.url },
'user needs to reconfirm so redirecting to reconfirm page'
)
req.session.reconfirm_email = user != null ? user.email : undefined
const redir = '/user/reconfirm'
if (
__guard__(req.headers != null ? req.headers['accept'] : undefined, x =>
x.match(/^application\/json.*$/)
)
) {
return res.json({ redir })
} else {
return res.redirect(redir)
}
},
_redirectToRegisterPage(req, res) {
logger.log(
{ url: req.url },
'user not logged in so redirecting to register page'
)
AuthenticationController.setRedirectInSession(req)
const url = `/register?${querystring.stringify(req.query)}`
res.redirect(url)
return Metrics.inc('security.login-redirect')
},
_recordSuccessfulLogin(userId, callback) {
if (callback == null) {
callback = function() {}
}
return UserUpdater.updateUser(
userId.toString(),
{
$set: { lastLoggedIn: new Date() },
$inc: { loginCount: 1 }
},
function(error) {
if (error != null) {
callback(error)
}
Metrics.inc('user.login.success')
return callback()
}
)
},
_recordFailedLogin(callback) {
Metrics.inc('user.login.failed')
if (callback) callback()
},
_getRedirectFromSession(req) {
let safePath
const value = __guard__(
req != null ? req.session : undefined,
x => x.postLoginRedirect
)
if (value) {
safePath = AuthenticationController._getSafeRedirectPath(value)
}
return safePath || null
},
_clearRedirectFromSession(req) {
if (req.session != null) {
return delete req.session.postLoginRedirect
}
},
_getSafeRedirectPath(value) {
const baseURL = Settings.siteUrl // base URL is required to construct URL from path
const url = new URL(value, baseURL)
let safePath = `${url.pathname}${url.search}${url.hash}`
if (safePath === '/') {
safePath = undefined
}
return safePath
}
})
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}