AuthenticationController.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. import AuthenticationManager from './AuthenticationManager.mjs'
  2. import SessionManager from './SessionManager.mjs'
  3. import OError from '@overleaf/o-error'
  4. import LoginRateLimiter from '../Security/LoginRateLimiter.mjs'
  5. import UserUpdater from '../User/UserUpdater.mjs'
  6. import Metrics from '@overleaf/metrics'
  7. import logger from '@overleaf/logger'
  8. import querystring from 'node:querystring'
  9. import Settings from '@overleaf/settings'
  10. import basicAuth from 'basic-auth'
  11. import tsscmp from 'tsscmp'
  12. import UserHandler from '../User/UserHandler.mjs'
  13. import UserSessionsManager from '../User/UserSessionsManager.mjs'
  14. import Analytics from '../Analytics/AnalyticsManager.mjs'
  15. import passport from 'passport'
  16. import NotificationsBuilder from '../Notifications/NotificationsBuilder.mjs'
  17. import UrlHelper from '../Helpers/UrlHelper.mjs'
  18. import AsyncFormHelper from '../Helpers/AsyncFormHelper.mjs'
  19. import _ from 'lodash'
  20. import UserAuditLogHandler from '../User/UserAuditLogHandler.mjs'
  21. import AnalyticsRegistrationSourceHelper from '../Analytics/AnalyticsRegistrationSourceHelper.mjs'
  22. import { acceptsJson } from '../../infrastructure/RequestContentTypeDetection.mjs'
  23. import AdminAuthorizationHelper from '../Helpers/AdminAuthorizationHelper.mjs'
  24. import Modules from '../../infrastructure/Modules.mjs'
  25. import { expressify, promisify } from '@overleaf/promise-utils'
  26. import { handleAuthenticateErrors } from './AuthenticationErrors.mjs'
  27. import EmailHelper from '../Helpers/EmailHelper.mjs'
  28. import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
  29. const { hasAdminAccess } = AdminAuthorizationHelper
  30. function send401WithChallenge(res) {
  31. res.setHeader('WWW-Authenticate', 'OverleafLogin')
  32. res.sendStatus(401)
  33. }
  34. function checkCredentials(userDetailsMap, user, password) {
  35. const expectedPassword = userDetailsMap.get(user)
  36. const userExists = userDetailsMap.has(user) && expectedPassword // user exists with a non-null password
  37. let isValid = false
  38. if (userExists) {
  39. if (Array.isArray(expectedPassword)) {
  40. const isValidPrimary = Boolean(
  41. expectedPassword[0] && tsscmp(expectedPassword[0], password)
  42. )
  43. const isValidFallback = Boolean(
  44. expectedPassword[1] && tsscmp(expectedPassword[1], password)
  45. )
  46. isValid = isValidPrimary || isValidFallback
  47. } else {
  48. isValid = tsscmp(expectedPassword, password)
  49. }
  50. }
  51. if (!isValid) {
  52. logger.err({ user }, 'invalid login details')
  53. }
  54. Metrics.inc('security.http-auth.check-credentials', 1, {
  55. path: userExists ? 'known-user' : 'unknown-user',
  56. status: isValid ? 'pass' : 'fail',
  57. })
  58. return isValid
  59. }
  60. // Map a thrown @node-oauth/oauth2-server error to a stable, machine-readable
  61. // code that callers (e.g. git-bridge) can switch on. err.name values come
  62. // from the library's error classes (snake_case OAuth standard names per
  63. // RFC 6749/6750). The token_expired distinction is driven by a marker we
  64. // set ourselves in Oauth2ServerModel.getAccessToken, so it survives library
  65. // upgrades that might change error_description text.
  66. function _classifyOauthError(err) {
  67. switch (err?.name) {
  68. case 'invalid_token':
  69. return err.overleafErrorCode === 'token_expired'
  70. ? 'token_expired'
  71. : 'token_invalid'
  72. case 'invalid_request':
  73. return err.overleafErrorCode === 'token_malformed'
  74. ? 'token_malformed'
  75. : 'invalid_request'
  76. case 'insufficient_scope':
  77. return 'insufficient_scope'
  78. case 'unauthorized_request':
  79. return 'unauthorized_request'
  80. default:
  81. return 'unknown'
  82. }
  83. }
  84. // TODO: Finish making these methods async
  85. const AuthenticationController = {
  86. serializeUser(user, callback) {
  87. if (!user._id || !user.email) {
  88. const err = new Error('serializeUser called with non-user object')
  89. logger.warn({ user }, err.message)
  90. return callback(err)
  91. }
  92. const lightUser = {
  93. _id: user._id,
  94. first_name: user.first_name,
  95. last_name: user.last_name,
  96. email: user.email,
  97. referal_id: user.referal_id,
  98. session_created: new Date().toISOString(),
  99. ip_address: user._login_req_ip,
  100. must_reconfirm: user.must_reconfirm,
  101. v1_id: user.overleaf != null ? user.overleaf.id : undefined,
  102. analyticsId: user.analyticsId || user._id,
  103. alphaProgram: user.alphaProgram || undefined, // only store if set
  104. betaProgram: user.betaProgram || undefined, // only store if set
  105. labsProgram: user.labsProgram, // always store, we could revert about 1 week after deploying this change.
  106. }
  107. if (user.isAdmin) {
  108. lightUser.isAdmin = true
  109. lightUser.adminRoles = user.adminRoles
  110. }
  111. callback(null, lightUser)
  112. },
  113. deserializeUser(user, cb) {
  114. cb(null, user)
  115. },
  116. passportLogin(req, res, next) {
  117. // This function is middleware which wraps the passport.authenticate middleware,
  118. // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
  119. // and send a `{redir: ""}` response on success
  120. passport.authenticate(
  121. 'local',
  122. { keepSessionInfo: true },
  123. async function (err, user, info) {
  124. if (err) {
  125. return next(err)
  126. }
  127. if (user) {
  128. // `user` is either a user object or false
  129. AuthenticationController.setAuditInfo(req, {
  130. method: 'Password login',
  131. })
  132. try {
  133. // We could investigate whether this can be done together with 'preFinishLogin' instead of being its own hook
  134. await Modules.promises.hooks.fire(
  135. 'saasLogin',
  136. { email: user.email },
  137. req
  138. )
  139. await AuthenticationController.promises.finishLogin(user, req, res)
  140. } catch (err) {
  141. return next(err)
  142. }
  143. } else {
  144. if (info.redir != null) {
  145. return res.json({ redir: info.redir })
  146. } else {
  147. res.status(info.status || 200)
  148. delete info.status
  149. const body = { message: info }
  150. const { errorReason } = info
  151. if (errorReason) {
  152. body.errorReason = errorReason
  153. delete info.errorReason
  154. }
  155. return res.json(body)
  156. }
  157. }
  158. }
  159. )(req, res, next)
  160. },
  161. async _finishLoginAsync(user, req, res) {
  162. if (user === false) {
  163. return AsyncFormHelper.redirect(req, res, '/login')
  164. } // OAuth2 'state' mismatch
  165. if (user.suspended) {
  166. return AsyncFormHelper.redirect(req, res, '/account-suspended')
  167. }
  168. if (Settings.adminOnlyLogin && !hasAdminAccess(user)) {
  169. return res.status(403).json({
  170. message: { type: 'error', text: 'Admin only panel' },
  171. })
  172. }
  173. const auditInfo = AuthenticationController.getAuditInfo(req)
  174. const anonymousAnalyticsId = req.session.analyticsId
  175. const isNewUser = req.session.justRegistered || false
  176. const results = await Modules.promises.hooks.fire(
  177. 'preFinishLogin',
  178. req,
  179. res,
  180. user
  181. )
  182. if (results.some(result => result && result.doNotFinish)) {
  183. return
  184. }
  185. if (user.must_reconfirm) {
  186. return AuthenticationController._redirectToReconfirmPage(req, res, user)
  187. }
  188. const redir =
  189. AuthenticationController.getRedirectFromSession(req) || '/project'
  190. _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser)
  191. const userId = user._id
  192. await UserAuditLogHandler.promises.addEntry(
  193. userId,
  194. 'login',
  195. userId,
  196. req.ip,
  197. auditInfo
  198. )
  199. await _afterLoginSessionSetupAsync(req, user)
  200. AuthenticationController._clearRedirectFromSession(req)
  201. AnalyticsRegistrationSourceHelper.clearSource(req.session)
  202. AnalyticsRegistrationSourceHelper.clearInbound(req.session)
  203. AsyncFormHelper.redirect(req, res, redir)
  204. },
  205. finishLogin(user, req, res, next) {
  206. AuthenticationController._finishLoginAsync(user, req, res).catch(err =>
  207. next(err)
  208. )
  209. },
  210. async doPassportLogin(req, username, password, done) {
  211. let user, info
  212. try {
  213. ;({ user, info } = await AuthenticationController._doPassportLogin(
  214. req,
  215. username,
  216. password
  217. ))
  218. } catch (error) {
  219. return done(error)
  220. }
  221. return done(undefined, user, info)
  222. },
  223. /**
  224. *
  225. * @param req
  226. * @param username
  227. * @param password
  228. * @returns {Promise<{ user: any, info: any}>}
  229. */
  230. async _doPassportLogin(req, username, password) {
  231. const email = EmailHelper.parseEmail(username)
  232. if (!email) {
  233. Metrics.inc('login_failure_reason', 1, { status: 'invalid_email' })
  234. return {
  235. user: null,
  236. info: {
  237. status: 400,
  238. type: 'error',
  239. text: req.i18n.translate('email_address_is_invalid'),
  240. },
  241. }
  242. }
  243. AuthenticationController.setAuditInfo(req, { method: 'Password login' })
  244. const { fromKnownDevice } = AuthenticationController.getAuditInfo(req)
  245. const auditLog = {
  246. ipAddress: req.ip,
  247. info: { method: 'Password login', fromKnownDevice },
  248. }
  249. let user, isPasswordReused
  250. try {
  251. ;({ user, isPasswordReused } =
  252. await AuthenticationManager.promises.authenticate(
  253. { email },
  254. password,
  255. auditLog,
  256. {
  257. enforceHIBPCheck: !fromKnownDevice,
  258. }
  259. ))
  260. } catch (error) {
  261. return {
  262. user: false,
  263. info: handleAuthenticateErrors(error, req),
  264. }
  265. }
  266. if (user && AuthenticationController.captchaRequiredForLogin(req, user)) {
  267. Metrics.inc('login_failure_reason', 1, { status: 'captcha_missing' })
  268. return {
  269. user: false,
  270. info: {
  271. text: req.i18n.translate('cannot_verify_user_not_robot'),
  272. type: 'error',
  273. errorReason: 'cannot_verify_user_not_robot',
  274. status: 400,
  275. },
  276. }
  277. } else if (user) {
  278. if (
  279. isPasswordReused &&
  280. AuthenticationController.getRedirectFromSession(req) == null
  281. ) {
  282. AuthenticationController.setRedirectInSession(
  283. req,
  284. '/compromised-password'
  285. )
  286. }
  287. // async actions
  288. return { user, info: undefined }
  289. } else {
  290. Metrics.inc('login_failure_reason', 1, { status: 'password_invalid' })
  291. AuthenticationController._recordFailedLogin()
  292. logger.debug({ email }, 'failed log in')
  293. return {
  294. user: false,
  295. info: {
  296. type: 'error',
  297. key: 'invalid-password-retry-or-reset',
  298. status: 401,
  299. },
  300. }
  301. }
  302. },
  303. captchaRequiredForLogin(req, user) {
  304. switch (AuthenticationController.getAuditInfo(req).captcha) {
  305. case 'trusted':
  306. case 'disabled':
  307. return false
  308. case 'solved':
  309. return false
  310. case 'skipped': {
  311. let required = false
  312. if (user.lastFailedLogin) {
  313. const requireCaptchaUntil =
  314. user.lastFailedLogin.getTime() +
  315. Settings.elevateAccountSecurityAfterFailedLogin
  316. required = requireCaptchaUntil >= Date.now()
  317. }
  318. Metrics.inc('force_captcha_on_login', 1, {
  319. status: required ? 'yes' : 'no',
  320. })
  321. return required
  322. }
  323. default:
  324. throw new Error('captcha middleware missing in handler chain')
  325. }
  326. },
  327. ipMatchCheck(req, user) {
  328. if (req.ip !== user.lastLoginIp) {
  329. NotificationsBuilder.ipMatcherAffiliation(user._id.toString()).create(
  330. req.ip,
  331. () => {}
  332. )
  333. }
  334. return UserUpdater.updateUser(
  335. user._id.toString(),
  336. {
  337. $set: { lastLoginIp: req.ip },
  338. },
  339. () => {}
  340. )
  341. },
  342. requireLogin() {
  343. const doRequest = function (req, res, next) {
  344. if (next == null) {
  345. next = function () {}
  346. }
  347. if (!SessionManager.isUserLoggedIn(req.session)) {
  348. if (acceptsJson(req)) return send401WithChallenge(res)
  349. return AuthenticationController._redirectToLoginOrRegisterPage(req, res)
  350. } else {
  351. req.user = SessionManager.getSessionUser(req.session)
  352. req.logger?.addFields({ userId: req.user._id })
  353. return next()
  354. }
  355. }
  356. return doRequest
  357. },
  358. /**
  359. * @param {string} scope
  360. * @return {import('express').Handler}
  361. */
  362. requireOauth(scope) {
  363. if (typeof scope !== 'string' || !scope) {
  364. throw new Error(
  365. "requireOauth() expects a non-empty string as 'scope' parameter"
  366. )
  367. }
  368. const middleware = async (req, res, next) => {
  369. const Oauth2Server = (
  370. await import('../../../../modules/oauth2-server/app/src/Oauth2Server.mjs')
  371. ).default
  372. const request = new Oauth2Server.Request(req)
  373. const response = new Oauth2Server.Response(res)
  374. try {
  375. const token = await Oauth2Server.server.authenticate(
  376. request,
  377. response,
  378. { scope }
  379. )
  380. req.oauth = { access_token: token.accessToken }
  381. req.oauth_token = token
  382. req.oauth_user = token.user
  383. next()
  384. } catch (err) {
  385. if (
  386. err.code === 400 &&
  387. err.message === 'Invalid request: malformed authorization header'
  388. ) {
  389. err.code = 401
  390. err.overleafErrorCode = 'token_malformed'
  391. }
  392. // send all other errors
  393. res.status(err.code).json({
  394. error: err.name,
  395. error_description: err.message,
  396. error_code: _classifyOauthError(err),
  397. })
  398. }
  399. }
  400. return expressify(middleware)
  401. },
  402. _globalLoginWhitelist: [],
  403. addEndpointToLoginWhitelist(endpoint) {
  404. return AuthenticationController._globalLoginWhitelist.push(endpoint)
  405. },
  406. requireGlobalLogin(req, res, next) {
  407. if (
  408. AuthenticationController._globalLoginWhitelist.includes(
  409. req._parsedUrl.pathname
  410. )
  411. ) {
  412. return next()
  413. }
  414. if (req.headers.authorization != null) {
  415. AuthenticationController.requirePrivateApiAuth()(req, res, next)
  416. } else if (SessionManager.isUserLoggedIn(req.session)) {
  417. next()
  418. } else {
  419. logger.debug(
  420. { url: req.url },
  421. 'user trying to access endpoint not in global whitelist'
  422. )
  423. if (acceptsJson(req)) return send401WithChallenge(res)
  424. AuthenticationController.setRedirectInSession(req)
  425. res.redirect('/login')
  426. }
  427. },
  428. validateAdmin(req, res, next) {
  429. const adminDomains = Settings.adminDomains
  430. if (
  431. !adminDomains ||
  432. !(Array.isArray(adminDomains) && adminDomains.length)
  433. ) {
  434. return next()
  435. }
  436. const user = SessionManager.getSessionUser(req.session)
  437. if (!hasAdminAccess(user)) {
  438. return next()
  439. }
  440. const email = user.email
  441. if (email == null) {
  442. return next(
  443. new OError('[ValidateAdmin] Admin user without email address', {
  444. userId: user._id,
  445. })
  446. )
  447. }
  448. if (!adminDomains.find(domain => email.endsWith(`@${domain}`))) {
  449. return next(
  450. new OError('[ValidateAdmin] Admin user with invalid email domain', {
  451. email,
  452. userId: user._id,
  453. })
  454. )
  455. }
  456. return next()
  457. },
  458. checkCredentials,
  459. requireBasicAuth: function (userDetails) {
  460. const userDetailsMap = new Map(Object.entries(userDetails))
  461. return function (req, res, next) {
  462. const credentials = basicAuth(req)
  463. if (
  464. !credentials ||
  465. !checkCredentials(userDetailsMap, credentials.name, credentials.pass)
  466. ) {
  467. send401WithChallenge(res)
  468. Metrics.inc('security.http-auth', 1, { status: 'reject' })
  469. } else {
  470. Metrics.inc('security.http-auth', 1, { status: 'accept' })
  471. next()
  472. }
  473. }
  474. },
  475. requirePrivateApiAuth() {
  476. return AuthenticationController.requireBasicAuth(Settings.httpAuthUsers)
  477. },
  478. setAuditInfo(req, info) {
  479. if (!req.__authAuditInfo) {
  480. req.__authAuditInfo = {}
  481. }
  482. Object.assign(req.__authAuditInfo, info)
  483. },
  484. getAuditInfo(req) {
  485. return req.__authAuditInfo || {}
  486. },
  487. setRedirectInSession(req, value) {
  488. if (value == null) {
  489. value =
  490. Object.keys(req.query).length > 0
  491. ? `${req.path}?${querystring.stringify(req.query)}`
  492. : `${req.path}`
  493. }
  494. if (
  495. req.session != null &&
  496. !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
  497. !/^.*\.(png|jpeg|svg)$/.test(value)
  498. ) {
  499. const safePath = UrlHelper.getSafeRedirectPath(value)
  500. return (req.session.postLoginRedirect = safePath)
  501. }
  502. },
  503. _redirectToLoginOrRegisterPage(req, res) {
  504. if (
  505. req.query.zipUrl != null ||
  506. req.session.sharedProjectData ||
  507. req.path === '/user/subscription/new'
  508. ) {
  509. AuthenticationController._redirectToRegisterPage(req, res)
  510. } else {
  511. AuthenticationController._redirectToLoginPage(req, res)
  512. }
  513. },
  514. _redirectToLoginPage(req, res) {
  515. logger.debug(
  516. { url: req.url },
  517. 'user not logged in so redirecting to login page'
  518. )
  519. AuthenticationController.setRedirectInSession(req)
  520. const url = `/login?${querystring.stringify(req.query)}`
  521. res.redirect(url)
  522. Metrics.inc('security.login-redirect')
  523. },
  524. _redirectToReconfirmPage(req, res, user) {
  525. logger.debug(
  526. { url: req.url },
  527. 'user needs to reconfirm so redirecting to reconfirm page'
  528. )
  529. req.session.reconfirm_email = user != null ? user.email : undefined
  530. const redir = '/user/reconfirm'
  531. AsyncFormHelper.redirect(req, res, redir)
  532. },
  533. _redirectToRegisterPage(req, res) {
  534. logger.debug(
  535. { url: req.url },
  536. 'user not logged in so redirecting to register page'
  537. )
  538. AuthenticationController.setRedirectInSession(req)
  539. const url = `/register?${querystring.stringify(req.query)}`
  540. res.redirect(url)
  541. Metrics.inc('security.login-redirect')
  542. },
  543. _recordSuccessfulLogin(userId, callback) {
  544. if (callback == null) {
  545. callback = function () {}
  546. }
  547. UserUpdater.updateUser(
  548. userId.toString(),
  549. {
  550. $set: { lastLoggedIn: new Date() },
  551. $inc: { loginCount: 1 },
  552. },
  553. function (error) {
  554. if (error != null) {
  555. callback(error)
  556. }
  557. Metrics.inc('user.login.success')
  558. callback()
  559. }
  560. )
  561. },
  562. _recordFailedLogin(callback) {
  563. Metrics.inc('user.login.failed')
  564. if (callback) callback()
  565. },
  566. getRedirectFromSession(req) {
  567. let safePath
  568. const value = _.get(req, ['session', 'postLoginRedirect'])
  569. if (value) {
  570. safePath = UrlHelper.getSafeRedirectPath(value)
  571. }
  572. return safePath || null
  573. },
  574. _clearRedirectFromSession(req) {
  575. if (req.session != null) {
  576. delete req.session.postLoginRedirect
  577. }
  578. },
  579. }
  580. function _afterLoginSessionSetup(req, user, callback) {
  581. req.login(user, { keepSessionInfo: true }, function (err) {
  582. if (err) {
  583. OError.tag(err, 'error from req.login', {
  584. user_id: user._id,
  585. })
  586. return callback(err)
  587. }
  588. delete req.session.__tmp
  589. delete req.session.csrfSecret
  590. // Populate the analyticsId cache in the session AFTER switching it into logged-in mode.
  591. req.session.analyticsId = user.analyticsId
  592. req.session.save(function (err) {
  593. if (err) {
  594. OError.tag(err, 'error saving regenerated session after login', {
  595. user_id: user._id,
  596. })
  597. return callback(err)
  598. }
  599. UserSessionsManager.trackSession(user, req.sessionID, function () {})
  600. if (!req.deviceHistory) {
  601. // Captcha disabled or SSO-based login.
  602. return callback()
  603. }
  604. req.deviceHistory.add(user.email)
  605. req.deviceHistory
  606. .serialize(req.res)
  607. .catch(err => {
  608. logger.err({ err }, 'cannot serialize deviceHistory')
  609. })
  610. .finally(() => callback())
  611. })
  612. })
  613. }
  614. const _afterLoginSessionSetupAsync = promisify(_afterLoginSessionSetup)
  615. function _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser) {
  616. UserHandler.promises.populateTeamInvites(user).catch(err => {
  617. logger.warn({ err }, 'error setting up login data')
  618. })
  619. SplitTestHandler.promises.userMaintenanceOnLogin(user).catch(err => {
  620. const userId = user._id
  621. logger.warn({ err, userId }, 'error cleaning up split-tests on login')
  622. })
  623. LoginRateLimiter.recordSuccessfulLogin(user.email, () => {})
  624. AuthenticationController._recordSuccessfulLogin(user._id, () => {})
  625. AuthenticationController.ipMatchCheck(req, user)
  626. Analytics.recordEventForUserInBackground(user._id, 'user-logged-in', {
  627. source: req.session.saml
  628. ? 'saml'
  629. : req.user_info?.auth_provider || 'email-password',
  630. })
  631. Analytics.identifyUser(
  632. user._id,
  633. anonymousAnalyticsId,
  634. isNewUser,
  635. Boolean(user.labsProgram)
  636. )
  637. logger.debug(
  638. { email: user.email, userId: user._id.toString() },
  639. 'successful log in'
  640. )
  641. req.session.justLoggedIn = true
  642. // capture the request ip for use when creating the session
  643. return (user._login_req_ip = req.ip)
  644. }
  645. AuthenticationController.promises = {
  646. finishLogin: AuthenticationController._finishLoginAsync,
  647. }
  648. export default AuthenticationController