AuthenticationController.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. const AuthenticationManager = require('./AuthenticationManager')
  2. const LoginRateLimiter = require('../Security/LoginRateLimiter')
  3. const UserUpdater = require('../User/UserUpdater')
  4. const Metrics = require('metrics-sharelatex')
  5. const logger = require('logger-sharelatex')
  6. const querystring = require('querystring')
  7. const Settings = require('settings-sharelatex')
  8. const basicAuth = require('basic-auth-connect')
  9. const crypto = require('crypto')
  10. const UserHandler = require('../User/UserHandler')
  11. const UserSessionsManager = require('../User/UserSessionsManager')
  12. const SessionStoreManager = require('../../infrastructure/SessionStoreManager')
  13. const Analytics = require('../Analytics/AnalyticsManager')
  14. const passport = require('passport')
  15. const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
  16. const UrlHelper = require('../Helpers/UrlHelper')
  17. const SudoModeHandler = require('../SudoMode/SudoModeHandler')
  18. const _ = require('lodash')
  19. const AuthenticationController = (module.exports = {
  20. serializeUser(user, callback) {
  21. if (!user._id || !user.email) {
  22. const err = new Error('serializeUser called with non-user object')
  23. logger.warn({ user }, err.message)
  24. return callback(err)
  25. }
  26. const lightUser = {
  27. _id: user._id,
  28. first_name: user.first_name,
  29. last_name: user.last_name,
  30. isAdmin: user.isAdmin,
  31. staffAccess: user.staffAccess,
  32. email: user.email,
  33. referal_id: user.referal_id,
  34. session_created: new Date().toISOString(),
  35. ip_address: user._login_req_ip,
  36. must_reconfirm: user.must_reconfirm,
  37. v1_id: user.overleaf != null ? user.overleaf.id : undefined
  38. }
  39. callback(null, lightUser)
  40. },
  41. deserializeUser(user, cb) {
  42. cb(null, user)
  43. },
  44. afterLoginSessionSetup(req, user, callback) {
  45. if (callback == null) {
  46. callback = function() {}
  47. }
  48. req.login(user, function(err) {
  49. if (err) {
  50. logger.warn({ user_id: user._id, err }, 'error from req.login')
  51. return callback(err)
  52. }
  53. // Regenerate the session to get a new sessionID (cookie value) to
  54. // protect against session fixation attacks
  55. const oldSession = req.session
  56. req.session.destroy(function(err) {
  57. if (err) {
  58. logger.warn(
  59. { user_id: user._id, err },
  60. 'error when trying to destroy old session'
  61. )
  62. return callback(err)
  63. }
  64. req.sessionStore.generate(req)
  65. // Note: the validation token is not writable, so it does not get
  66. // transferred to the new session below.
  67. for (let key in oldSession) {
  68. const value = oldSession[key]
  69. if (key !== '__tmp') {
  70. req.session[key] = value
  71. }
  72. }
  73. req.session.save(function(err) {
  74. if (err) {
  75. logger.warn(
  76. { user_id: user._id },
  77. 'error saving regenerated session after login'
  78. )
  79. return callback(err)
  80. }
  81. UserSessionsManager.trackSession(user, req.sessionID, function() {})
  82. callback(null)
  83. })
  84. })
  85. })
  86. },
  87. passportLogin(req, res, next) {
  88. // This function is middleware which wraps the passport.authenticate middleware,
  89. // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
  90. // and send a `{redir: ""}` response on success
  91. passport.authenticate('local', function(err, user, info) {
  92. if (err) {
  93. return next(err)
  94. }
  95. if (user) {
  96. // `user` is either a user object or false
  97. return AuthenticationController.finishLogin(user, req, res, next)
  98. } else {
  99. if (info.redir != null) {
  100. return res.json({ redir: info.redir })
  101. } else {
  102. return res.json({ message: info })
  103. }
  104. }
  105. })(req, res, next)
  106. },
  107. finishLogin(user, req, res, next) {
  108. if (user === false) {
  109. return res.redirect('/login')
  110. } // OAuth2 'state' mismatch
  111. if (user.must_reconfirm) {
  112. return AuthenticationController._redirectToReconfirmPage(req, res, user)
  113. }
  114. const redir =
  115. AuthenticationController._getRedirectFromSession(req) || '/project'
  116. AuthenticationController._loginAsyncHandlers(req, user)
  117. AuthenticationController.afterLoginSessionSetup(req, user, function(err) {
  118. if (err) {
  119. return next(err)
  120. }
  121. SudoModeHandler.activateSudoMode(user._id, function(err) {
  122. if (err) {
  123. logger.err(
  124. { err, user_id: user._id },
  125. 'Error activating Sudo Mode on login, continuing'
  126. )
  127. }
  128. AuthenticationController._clearRedirectFromSession(req)
  129. if (
  130. _.get(req, ['headers', 'accept'], '').match(/^application\/json.*$/)
  131. ) {
  132. res.json({ redir })
  133. } else {
  134. res.redirect(redir)
  135. }
  136. })
  137. })
  138. },
  139. doPassportLogin(req, username, password, done) {
  140. const email = username.toLowerCase()
  141. const Modules = require('../../infrastructure/Modules')
  142. Modules.hooks.fire('preDoPassportLogin', req, email, function(
  143. err,
  144. infoList
  145. ) {
  146. if (err) {
  147. return done(err)
  148. }
  149. const info = infoList.find(i => i != null)
  150. if (info != null) {
  151. return done(null, false, info)
  152. }
  153. LoginRateLimiter.processLoginRequest(email, function(err, isAllowed) {
  154. if (err) {
  155. return done(err)
  156. }
  157. if (!isAllowed) {
  158. logger.log({ email }, 'too many login requests')
  159. return done(null, null, {
  160. text: req.i18n.translate('to_many_login_requests_2_mins'),
  161. type: 'error'
  162. })
  163. }
  164. AuthenticationManager.authenticate({ email }, password, function(
  165. error,
  166. user
  167. ) {
  168. if (error != null) {
  169. return done(error)
  170. }
  171. if (user != null) {
  172. // async actions
  173. done(null, user)
  174. } else {
  175. AuthenticationController._recordFailedLogin()
  176. logger.log({ email }, 'failed log in')
  177. done(null, false, {
  178. text: req.i18n.translate('email_or_password_wrong_try_again'),
  179. type: 'error'
  180. })
  181. }
  182. })
  183. })
  184. })
  185. },
  186. _loginAsyncHandlers(req, user) {
  187. UserHandler.setupLoginData(user, err => {
  188. if (err != null) {
  189. logger.warn({ err }, 'error setting up login data')
  190. }
  191. })
  192. LoginRateLimiter.recordSuccessfulLogin(user.email)
  193. AuthenticationController._recordSuccessfulLogin(user._id)
  194. AuthenticationController.ipMatchCheck(req, user)
  195. Analytics.recordEvent(user._id, 'user-logged-in', { ip: req.ip })
  196. Analytics.identifyUser(user._id, req.sessionID)
  197. logger.log(
  198. { email: user.email, user_id: user._id.toString() },
  199. 'successful log in'
  200. )
  201. req.session.justLoggedIn = true
  202. // capture the request ip for use when creating the session
  203. return (user._login_req_ip = req.ip)
  204. },
  205. ipMatchCheck(req, user) {
  206. if (req.ip !== user.lastLoginIp) {
  207. NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip)
  208. }
  209. return UserUpdater.updateUser(user._id.toString(), {
  210. $set: { lastLoginIp: req.ip }
  211. })
  212. },
  213. setInSessionUser(req, props) {
  214. const sessionUser = AuthenticationController.getSessionUser(req)
  215. if (!sessionUser) {
  216. return
  217. }
  218. for (let key in props) {
  219. const value = props[key]
  220. sessionUser[key] = value
  221. }
  222. return null
  223. },
  224. isUserLoggedIn(req) {
  225. const userId = AuthenticationController.getLoggedInUserId(req)
  226. return ![null, undefined, false].includes(userId)
  227. },
  228. // TODO: perhaps should produce an error if the current user is not present
  229. getLoggedInUserId(req) {
  230. const user = AuthenticationController.getSessionUser(req)
  231. if (user) {
  232. return user._id
  233. } else {
  234. return null
  235. }
  236. },
  237. getLoggedInUserV1Id(req) {
  238. const user = AuthenticationController.getSessionUser(req)
  239. if ((user != null ? user.v1_id : undefined) != null) {
  240. return user.v1_id
  241. } else {
  242. return null
  243. }
  244. },
  245. getSessionUser(req) {
  246. const sessionUser = _.get(req, ['session', 'user'])
  247. const sessionPassportUser = _.get(req, ['session', 'passport', 'user'])
  248. return sessionUser || sessionPassportUser || null
  249. },
  250. requireLogin() {
  251. const doRequest = function(req, res, next) {
  252. if (next == null) {
  253. next = function() {}
  254. }
  255. if (!AuthenticationController.isUserLoggedIn(req)) {
  256. return AuthenticationController._redirectToLoginOrRegisterPage(req, res)
  257. } else {
  258. req.user = AuthenticationController.getSessionUser(req)
  259. return next()
  260. }
  261. }
  262. return doRequest
  263. },
  264. requireOauth() {
  265. // require this here because module may not be included in some versions
  266. const Oauth2Server = require('../../../../modules/oauth2-server/app/src/Oauth2Server')
  267. return function(req, res, next) {
  268. if (next == null) {
  269. next = function() {}
  270. }
  271. const request = new Oauth2Server.Request(req)
  272. const response = new Oauth2Server.Response(res)
  273. return Oauth2Server.server.authenticate(request, response, {}, function(
  274. err,
  275. token
  276. ) {
  277. if (err) {
  278. // use a 401 status code for malformed header for git-bridge
  279. if (
  280. err.code === 400 &&
  281. err.message === 'Invalid request: malformed authorization header'
  282. ) {
  283. err.code = 401
  284. }
  285. // send all other errors
  286. return res
  287. .status(err.code)
  288. .json({ error: err.name, error_description: err.message })
  289. }
  290. req.oauth = { access_token: token.accessToken }
  291. req.oauth_token = token
  292. req.oauth_user = token.user
  293. return next()
  294. })
  295. }
  296. },
  297. validateUserSession: function() {
  298. // Middleware to check that the user's session is still good on key actions,
  299. // such as opening a a project. Could be used to check that session has not
  300. // exceeded a maximum lifetime (req.session.session_created), or for session
  301. // hijacking checks (e.g. change of ip address, req.session.ip_address). For
  302. // now, just check that the session has been loaded from the session store
  303. // correctly.
  304. return function(req, res, next) {
  305. // check that the session store is returning valid results
  306. if (req.session && !SessionStoreManager.hasValidationToken(req)) {
  307. // force user to update session
  308. req.session.regenerate(() => {
  309. // need to destroy the existing session and generate a new one
  310. // otherwise they will already be logged in when they are redirected
  311. // to the login page
  312. AuthenticationController._redirectToLoginOrRegisterPage(req, res)
  313. })
  314. } else {
  315. next()
  316. }
  317. }
  318. },
  319. _globalLoginWhitelist: [],
  320. addEndpointToLoginWhitelist(endpoint) {
  321. return AuthenticationController._globalLoginWhitelist.push(endpoint)
  322. },
  323. requireGlobalLogin(req, res, next) {
  324. if (
  325. AuthenticationController._globalLoginWhitelist.includes(
  326. req._parsedUrl.pathname
  327. )
  328. ) {
  329. return next()
  330. }
  331. if (req.headers['authorization'] != null) {
  332. AuthenticationController.httpAuth(req, res, next)
  333. } else if (AuthenticationController.isUserLoggedIn(req)) {
  334. next()
  335. } else {
  336. logger.log(
  337. { url: req.url },
  338. 'user trying to access endpoint not in global whitelist'
  339. )
  340. AuthenticationController.setRedirectInSession(req)
  341. res.redirect('/login')
  342. }
  343. },
  344. httpAuth: basicAuth(function(user, pass) {
  345. let expectedPassword = Settings.httpAuthUsers[user]
  346. const isValid =
  347. expectedPassword &&
  348. expectedPassword.length === pass.length &&
  349. crypto.timingSafeEqual(Buffer.from(expectedPassword), Buffer.from(pass))
  350. if (!isValid) {
  351. logger.err({ user, pass }, 'invalid login details')
  352. }
  353. return isValid
  354. }),
  355. setRedirectInSession(req, value) {
  356. if (value == null) {
  357. value =
  358. Object.keys(req.query).length > 0
  359. ? `${req.path}?${querystring.stringify(req.query)}`
  360. : `${req.path}`
  361. }
  362. if (
  363. req.session != null &&
  364. !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
  365. !/^.*\.(png|jpeg|svg)$/.test(value)
  366. ) {
  367. const safePath = UrlHelper.getSafeRedirectPath(value)
  368. return (req.session.postLoginRedirect = safePath)
  369. }
  370. },
  371. _redirectToLoginOrRegisterPage(req, res) {
  372. if (
  373. req.query.zipUrl != null ||
  374. req.query.project_name != null ||
  375. req.path === '/user/subscription/new'
  376. ) {
  377. AuthenticationController._redirectToRegisterPage(req, res)
  378. } else {
  379. AuthenticationController._redirectToLoginPage(req, res)
  380. }
  381. },
  382. _redirectToLoginPage(req, res) {
  383. logger.log(
  384. { url: req.url },
  385. 'user not logged in so redirecting to login page'
  386. )
  387. AuthenticationController.setRedirectInSession(req)
  388. const url = `/login?${querystring.stringify(req.query)}`
  389. res.redirect(url)
  390. Metrics.inc('security.login-redirect')
  391. },
  392. _redirectToReconfirmPage(req, res, user) {
  393. logger.log(
  394. { url: req.url },
  395. 'user needs to reconfirm so redirecting to reconfirm page'
  396. )
  397. req.session.reconfirm_email = user != null ? user.email : undefined
  398. const redir = '/user/reconfirm'
  399. if (_.get(req, ['headers', 'accept'], '').match(/^application\/json.*$/)) {
  400. res.json({ redir })
  401. } else {
  402. res.redirect(redir)
  403. }
  404. },
  405. _redirectToRegisterPage(req, res) {
  406. logger.log(
  407. { url: req.url },
  408. 'user not logged in so redirecting to register page'
  409. )
  410. AuthenticationController.setRedirectInSession(req)
  411. const url = `/register?${querystring.stringify(req.query)}`
  412. res.redirect(url)
  413. Metrics.inc('security.login-redirect')
  414. },
  415. _recordSuccessfulLogin(userId, callback) {
  416. if (callback == null) {
  417. callback = function() {}
  418. }
  419. UserUpdater.updateUser(
  420. userId.toString(),
  421. {
  422. $set: { lastLoggedIn: new Date() },
  423. $inc: { loginCount: 1 }
  424. },
  425. function(error) {
  426. if (error != null) {
  427. callback(error)
  428. }
  429. Metrics.inc('user.login.success')
  430. callback()
  431. }
  432. )
  433. },
  434. _recordFailedLogin(callback) {
  435. Metrics.inc('user.login.failed')
  436. if (callback) callback()
  437. },
  438. _getRedirectFromSession(req) {
  439. let safePath
  440. const value = _.get(req, ['session', 'postLoginRedirect'])
  441. if (value) {
  442. safePath = UrlHelper.getSafeRedirectPath(value)
  443. }
  444. return safePath || null
  445. },
  446. _clearRedirectFromSession(req) {
  447. if (req.session != null) {
  448. delete req.session.postLoginRedirect
  449. }
  450. }
  451. })