AuthenticationControllerTests.js 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const modulePath =
  4. '../../../../app/src/Features/Authentication/AuthenticationController.js'
  5. const SandboxedModule = require('sandboxed-module')
  6. const tk = require('timekeeper')
  7. const MockRequest = require('../helpers/MockRequest')
  8. const MockResponse = require('../helpers/MockResponse')
  9. const { ObjectId } = require('mongodb-legacy')
  10. const AuthenticationErrors = require('../../../../app/src/Features/Authentication/AuthenticationErrors')
  11. describe('AuthenticationController', function () {
  12. beforeEach(function () {
  13. tk.freeze(Date.now())
  14. this.UserModel = { findOne: sinon.stub() }
  15. this.httpAuthUsers = {
  16. 'valid-test-user': Math.random().toString(16).slice(2),
  17. }
  18. this.user = {
  19. _id: new ObjectId(),
  20. email: (this.email = 'USER@example.com'),
  21. first_name: 'bob',
  22. last_name: 'brown',
  23. referal_id: 1234,
  24. isAdmin: false,
  25. }
  26. this.staffUser = {
  27. ...this.user,
  28. staffAccess: {
  29. publisherMetrics: true,
  30. publisherManagement: false,
  31. institutionMetrics: true,
  32. institutionManagement: false,
  33. groupMetrics: true,
  34. groupManagement: false,
  35. adminMetrics: true,
  36. splitTestMetrics: false,
  37. splitTestManagement: true,
  38. },
  39. }
  40. this.noStaffAccessUser = {
  41. ...this.user,
  42. staffAccess: {
  43. publisherMetrics: false,
  44. publisherManagement: false,
  45. institutionMetrics: false,
  46. institutionManagement: false,
  47. groupMetrics: false,
  48. groupManagement: false,
  49. adminMetrics: false,
  50. splitTestMetrics: false,
  51. splitTestManagement: false,
  52. },
  53. }
  54. this.password = 'banana'
  55. this.req = new MockRequest()
  56. this.res = new MockResponse()
  57. this.callback = sinon.stub()
  58. this.next = sinon.stub()
  59. this.req.session.analyticsId = 'abc-123'
  60. this.AuthenticationController = SandboxedModule.require(modulePath, {
  61. requires: {
  62. '../Helpers/AdminAuthorizationHelper': (this.AdminAuthorizationHelper =
  63. {
  64. hasAdminAccess: sinon.stub().returns(false),
  65. }),
  66. './AuthenticationErrors': AuthenticationErrors,
  67. '../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
  68. addEntry: sinon.stub().yields(null),
  69. promises: {
  70. addEntry: sinon.stub().resolves(),
  71. },
  72. }),
  73. '../Helpers/AsyncFormHelper': (this.AsyncFormHelper = {
  74. redirect: sinon.stub(),
  75. }),
  76. '../../infrastructure/RequestContentTypeDetection': {
  77. acceptsJson: (this.acceptsJson = sinon.stub().returns(false)),
  78. },
  79. './AuthenticationManager': (this.AuthenticationManager = {
  80. promises: {},
  81. }),
  82. '../User/UserUpdater': (this.UserUpdater = {
  83. updateUser: sinon.stub(),
  84. }),
  85. '@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }),
  86. '../Security/LoginRateLimiter': (this.LoginRateLimiter = {
  87. processLoginRequest: sinon.stub(),
  88. recordSuccessfulLogin: sinon.stub(),
  89. promises: {
  90. processLoginRequest: sinon.stub(),
  91. recordSuccessfulLogin: sinon.stub(),
  92. },
  93. }),
  94. '../User/UserHandler': (this.UserHandler = {
  95. populateTeamInvites: sinon.stub(),
  96. }),
  97. '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
  98. recordEventForUserInBackground: sinon.stub(),
  99. identifyUser: sinon.stub(),
  100. getIdsFromSession: sinon.stub().returns({ userId: this.user._id }),
  101. }),
  102. '@overleaf/settings': (this.Settings = {
  103. siteUrl: 'http://www.foo.bar',
  104. httpAuthUsers: this.httpAuthUsers,
  105. elevateAccountSecurityAfterFailedLogin: 90 * 24 * 60 * 60 * 1000,
  106. }),
  107. passport: (this.passport = {
  108. authenticate: sinon.stub().returns(sinon.stub()),
  109. }),
  110. '../User/UserSessionsManager': (this.UserSessionsManager = {
  111. trackSession: sinon.stub(),
  112. untrackSession: sinon.stub(),
  113. removeSessionsFromRedis: sinon.stub().yields(null),
  114. }),
  115. '../../infrastructure/Modules': (this.Modules = {
  116. hooks: { fire: sinon.stub().yields(null, []) },
  117. promises: { hooks: { fire: sinon.stub().resolves([]) } },
  118. }),
  119. '../Notifications/NotificationsBuilder': (this.NotificationsBuilder = {
  120. ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }),
  121. }),
  122. '../../models/User': { User: this.UserModel },
  123. '../../../../modules/oauth2-server/app/src/Oauth2Server':
  124. (this.Oauth2Server = {
  125. Request: sinon.stub(),
  126. Response: sinon.stub(),
  127. server: {
  128. authenticate: sinon.stub(),
  129. },
  130. }),
  131. '../Helpers/UrlHelper': (this.UrlHelper = {
  132. getSafeRedirectPath: sinon.stub(),
  133. }),
  134. './SessionManager': (this.SessionManager = {
  135. isUserLoggedIn: sinon.stub().returns(true),
  136. getSessionUser: sinon.stub().returns(this.user),
  137. }),
  138. },
  139. })
  140. this.UrlHelper.getSafeRedirectPath
  141. .withArgs('https://evil.com')
  142. .returns(undefined)
  143. this.UrlHelper.getSafeRedirectPath.returnsArg(0)
  144. })
  145. afterEach(function () {
  146. tk.reset()
  147. })
  148. describe('validateAdmin', function () {
  149. beforeEach(function () {
  150. this.Settings.adminDomains = ['good.example.com']
  151. this.goodAdmin = {
  152. email: 'alice@good.example.com',
  153. isAdmin: true,
  154. }
  155. this.badAdmin = {
  156. email: 'beatrice@bad.example.com',
  157. isAdmin: true,
  158. }
  159. this.normalUser = {
  160. email: 'claire@whatever.example.com',
  161. isAdmin: false,
  162. }
  163. })
  164. it('should skip when adminDomains are not configured', function (done) {
  165. this.Settings.adminDomains = []
  166. this.SessionManager.getSessionUser = sinon.stub().returns(this.normalUser)
  167. this.AuthenticationController.validateAdmin(this.req, this.res, err => {
  168. this.SessionManager.getSessionUser.called.should.equal(false)
  169. expect(err).to.not.exist
  170. done()
  171. })
  172. })
  173. it('should skip non-admin user', function (done) {
  174. this.SessionManager.getSessionUser = sinon.stub().returns(this.normalUser)
  175. this.AuthenticationController.validateAdmin(this.req, this.res, err => {
  176. this.SessionManager.getSessionUser.called.should.equal(true)
  177. expect(err).to.not.exist
  178. done()
  179. })
  180. })
  181. it('should permit an admin with the right doman', function (done) {
  182. this.SessionManager.getSessionUser = sinon.stub().returns(this.goodAdmin)
  183. this.AuthenticationController.validateAdmin(this.req, this.res, err => {
  184. this.SessionManager.getSessionUser.called.should.equal(true)
  185. expect(err).to.not.exist
  186. done()
  187. })
  188. })
  189. it('should block an admin with a missing email', function (done) {
  190. this.SessionManager.getSessionUser = sinon
  191. .stub()
  192. .returns({ isAdmin: true })
  193. this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
  194. this.AuthenticationController.validateAdmin(this.req, this.res, err => {
  195. this.SessionManager.getSessionUser.called.should.equal(true)
  196. expect(err).to.exist
  197. done()
  198. })
  199. })
  200. it('should block an admin with a bad domain', function (done) {
  201. this.SessionManager.getSessionUser = sinon.stub().returns(this.badAdmin)
  202. this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
  203. this.AuthenticationController.validateAdmin(this.req, this.res, err => {
  204. this.SessionManager.getSessionUser.called.should.equal(true)
  205. expect(err).to.exist
  206. done()
  207. })
  208. })
  209. })
  210. describe('serializeUser', function () {
  211. describe('when isAdmin is false', function () {
  212. it('does not return an isAdmin field', function () {
  213. const isAdminMatcher = sinon.match(value => {
  214. return !('isAdmin' in value)
  215. })
  216. this.AuthenticationController.serializeUser(this.user, this.callback)
  217. expect(this.callback).to.have.been.calledWith(null, isAdminMatcher)
  218. })
  219. })
  220. describe('when staffAccess fields are provided', function () {
  221. it('only returns the fields set to true', function () {
  222. const expectedStaffAccess = {
  223. publisherMetrics: true,
  224. institutionMetrics: true,
  225. groupMetrics: true,
  226. adminMetrics: true,
  227. splitTestManagement: true,
  228. }
  229. const staffAccessMatcher = sinon.match(value => {
  230. return (
  231. Object.keys(value.staffAccess).length ===
  232. Object.keys(expectedStaffAccess).length
  233. )
  234. })
  235. this.AuthenticationController.serializeUser(
  236. this.staffUser,
  237. this.callback
  238. )
  239. expect(this.callback).to.have.been.calledWith(null, staffAccessMatcher)
  240. })
  241. })
  242. describe('when all staffAccess fields are false', function () {
  243. it('no staffAccess attribute is set', function () {
  244. const staffAccessMatcher = sinon.match(value => {
  245. return !('staffAccess' in value)
  246. })
  247. this.AuthenticationController.serializeUser(
  248. this.noStaffAccessUser,
  249. this.callback
  250. )
  251. expect(this.callback).to.have.been.calledWith(null, staffAccessMatcher)
  252. })
  253. })
  254. })
  255. describe('passportLogin', function () {
  256. beforeEach(function () {
  257. this.info = null
  258. this.req.login = sinon.stub().yields(null)
  259. this.res.json = sinon.stub()
  260. this.req.session = {
  261. passport: { user: this.user },
  262. postLoginRedirect: '/path/to/redir/to',
  263. }
  264. this.req.session.destroy = sinon.stub().yields(null)
  265. this.req.session.save = sinon.stub().yields(null)
  266. this.req.sessionStore = { generate: sinon.stub() }
  267. this.AuthenticationController.promises.finishLogin = sinon.stub()
  268. this.passport.authenticate.yields(null, this.user, this.info)
  269. this.err = new Error('woops')
  270. })
  271. it('should call passport.authenticate', function () {
  272. this.AuthenticationController.passportLogin(this.req, this.res, this.next)
  273. this.passport.authenticate.callCount.should.equal(1)
  274. })
  275. describe('when authenticate produces an error', function () {
  276. beforeEach(function () {
  277. this.passport.authenticate.yields(this.err)
  278. })
  279. it('should return next with an error', function () {
  280. this.AuthenticationController.passportLogin(
  281. this.req,
  282. this.res,
  283. this.next
  284. )
  285. this.next.calledWith(this.err).should.equal(true)
  286. })
  287. })
  288. describe('when authenticate produces a user', function () {
  289. beforeEach(function () {
  290. this.req.session.postLoginRedirect = 'some_redirect'
  291. this.passport.authenticate.yields(null, this.user, this.info)
  292. })
  293. afterEach(function () {
  294. delete this.req.session.postLoginRedirect
  295. })
  296. it('should call finishLogin', function (done) {
  297. this.AuthenticationController.promises.finishLogin.callsFake(() => {
  298. this.AuthenticationController.promises.finishLogin.callCount.should.equal(
  299. 1
  300. )
  301. this.AuthenticationController.promises.finishLogin
  302. .calledWith(this.user, this.req, this.res)
  303. .should.equal(true)
  304. done()
  305. })
  306. this.AuthenticationController.passportLogin(
  307. this.req,
  308. this.res,
  309. this.next
  310. )
  311. })
  312. })
  313. describe('when authenticate does not produce a user', function () {
  314. beforeEach(function () {
  315. this.info = { text: 'a', type: 'b' }
  316. this.passport.authenticate.yields(null, false, this.info)
  317. })
  318. it('should not call finishLogin', function () {
  319. this.AuthenticationController.passportLogin(
  320. this.req,
  321. this.res,
  322. this.next
  323. )
  324. this.AuthenticationController.promises.finishLogin.callCount.should.equal(
  325. 0
  326. )
  327. })
  328. it('should not send a json response with redirect', function () {
  329. this.AuthenticationController.passportLogin(
  330. this.req,
  331. this.res,
  332. this.next
  333. )
  334. this.res.json.callCount.should.equal(1)
  335. this.res.json.should.have.been.calledWith({ message: this.info })
  336. expect(this.res.json.lastCall.args[0].redir != null).to.equal(false)
  337. })
  338. })
  339. })
  340. describe('doPassportLogin', function () {
  341. beforeEach(function () {
  342. this.AuthenticationController._recordFailedLogin = sinon.stub()
  343. this.AuthenticationController._recordSuccessfulLogin = sinon.stub()
  344. this.req.body = {
  345. email: this.email,
  346. password: this.password,
  347. session: {
  348. postLoginRedirect: '/path/to/redir/to',
  349. },
  350. }
  351. this.req.__authAuditInfo = { captcha: 'disabled' }
  352. this.cb = sinon.stub()
  353. })
  354. describe('when the authentication errors', function () {
  355. beforeEach(function () {
  356. this.LoginRateLimiter.promises.processLoginRequest.resolves(true)
  357. this.errorsWith = (error, done) => {
  358. this.AuthenticationManager.promises.authenticate = sinon
  359. .stub()
  360. .rejects(error)
  361. this.AuthenticationController.doPassportLogin(
  362. this.req,
  363. this.req.body.email,
  364. this.req.body.password,
  365. this.cb.callsFake(() => done())
  366. )
  367. }
  368. })
  369. describe('with "password is too long"', function () {
  370. beforeEach(function (done) {
  371. this.errorsWith(new Error('password is too long'), done)
  372. })
  373. it('should send a 429', function () {
  374. this.cb.should.have.been.calledWith(undefined, false, {
  375. status: 422,
  376. type: 'error',
  377. key: 'password-too-long',
  378. text: 'password_too_long_please_reset',
  379. })
  380. })
  381. })
  382. describe('with ParallelLoginError', function () {
  383. beforeEach(function (done) {
  384. this.errorsWith(new AuthenticationErrors.ParallelLoginError(), done)
  385. })
  386. it('should send a 429', function () {
  387. this.cb.should.have.been.calledWith(undefined, false, {
  388. status: 429,
  389. })
  390. })
  391. })
  392. describe('with PasswordReusedError', function () {
  393. beforeEach(function (done) {
  394. this.errorsWith(new AuthenticationErrors.PasswordReusedError(), done)
  395. })
  396. it('should send a 400', function () {
  397. this.cb.should.have.been.calledWith(undefined, false, {
  398. status: 400,
  399. type: 'error',
  400. key: 'password-compromised',
  401. text: 'password_compromised_try_again_or_use_known_device_or_reset.',
  402. })
  403. })
  404. })
  405. describe('with another error', function () {
  406. const err = new Error('unhandled error')
  407. beforeEach(function (done) {
  408. this.errorsWith(err, done)
  409. })
  410. it('should send a 400', function () {
  411. this.cb.should.have.been.calledWith(err)
  412. })
  413. })
  414. })
  415. describe('when the user is authenticated', function () {
  416. beforeEach(function () {
  417. this.cb = sinon.stub()
  418. this.LoginRateLimiter.promises.processLoginRequest.resolves(true)
  419. this.AuthenticationManager.promises.authenticate = sinon
  420. .stub()
  421. .resolves({ user: this.user })
  422. this.req.sessionID = Math.random()
  423. })
  424. describe('happy path', function () {
  425. beforeEach(function (done) {
  426. this.AuthenticationController.doPassportLogin(
  427. this.req,
  428. this.req.body.email,
  429. this.req.body.password,
  430. this.cb.callsFake(() => done())
  431. )
  432. })
  433. it('should attempt to authorise the user', function () {
  434. this.AuthenticationManager.promises.authenticate
  435. .calledWith({ email: this.email.toLowerCase() }, this.password)
  436. .should.equal(true)
  437. })
  438. it("should establish the user's session", function () {
  439. this.cb.calledWith(undefined, this.user).should.equal(true)
  440. })
  441. })
  442. describe('with a user having a recent failed login ', function () {
  443. beforeEach(function () {
  444. this.user.lastFailedLogin = new Date()
  445. })
  446. describe('with captcha disabled', function () {
  447. beforeEach(function (done) {
  448. this.req.__authAuditInfo.captcha = 'disabled'
  449. this.AuthenticationController.doPassportLogin(
  450. this.req,
  451. this.req.body.email,
  452. this.req.body.password,
  453. this.cb.callsFake(() => done())
  454. )
  455. })
  456. it('should let the user log in', function () {
  457. this.cb.should.have.been.calledWith(undefined, this.user)
  458. })
  459. })
  460. describe('with a solved captcha', function () {
  461. beforeEach(function (done) {
  462. this.req.__authAuditInfo.captcha = 'solved'
  463. this.AuthenticationController.doPassportLogin(
  464. this.req,
  465. this.req.body.email,
  466. this.req.body.password,
  467. this.cb.callsFake(() => done())
  468. )
  469. })
  470. it('should let the user log in', function () {
  471. this.cb.should.have.been.calledWith(undefined, this.user)
  472. })
  473. })
  474. describe('with a skipped captcha', function () {
  475. beforeEach(function (done) {
  476. this.req.__authAuditInfo.captcha = 'skipped'
  477. this.AuthenticationController.doPassportLogin(
  478. this.req,
  479. this.req.body.email,
  480. this.req.body.password,
  481. this.cb.callsFake(() => done())
  482. )
  483. })
  484. it('should request a captcha', function () {
  485. this.cb.should.have.been.calledWith(undefined, false, {
  486. text: 'cannot_verify_user_not_robot',
  487. type: 'error',
  488. errorReason: 'cannot_verify_user_not_robot',
  489. status: 400,
  490. })
  491. })
  492. })
  493. })
  494. })
  495. describe('when the user is not authenticated', function () {
  496. beforeEach(function (done) {
  497. this.LoginRateLimiter.promises.processLoginRequest.resolves(true)
  498. this.AuthenticationManager.promises.authenticate = sinon
  499. .stub()
  500. .resolves({ user: null })
  501. this.cb = sinon.stub().callsFake(() => done())
  502. this.AuthenticationController.doPassportLogin(
  503. this.req,
  504. this.req.body.email,
  505. this.req.body.password,
  506. this.cb
  507. )
  508. })
  509. it('should not establish the login', function () {
  510. this.cb.callCount.should.equal(1)
  511. this.cb.calledWith(null, false)
  512. expect(this.cb.lastCall.args[2]).to.deep.equal({
  513. type: 'error',
  514. key: 'invalid-password-retry-or-reset',
  515. status: 401,
  516. })
  517. })
  518. it('should not setup the user data in the background', function () {
  519. this.UserHandler.populateTeamInvites.called.should.equal(false)
  520. })
  521. it('should record a failed login', function () {
  522. this.AuthenticationController._recordFailedLogin.called.should.equal(
  523. true
  524. )
  525. })
  526. it('should log the failed login', function () {
  527. this.logger.debug
  528. .calledWith({ email: this.email.toLowerCase() }, 'failed log in')
  529. .should.equal(true)
  530. })
  531. })
  532. })
  533. describe('requireLogin', function () {
  534. beforeEach(function () {
  535. this.user = {
  536. _id: 'user-id-123',
  537. email: 'user@overleaf.com',
  538. }
  539. this.middleware = this.AuthenticationController.requireLogin()
  540. })
  541. describe('when the user is logged in', function () {
  542. beforeEach(function () {
  543. this.req.session = {
  544. user: (this.user = {
  545. _id: 'user-id-123',
  546. email: 'user@overleaf.com',
  547. }),
  548. }
  549. this.middleware(this.req, this.res, this.next)
  550. })
  551. it('should call the next method in the chain', function () {
  552. this.next.called.should.equal(true)
  553. })
  554. })
  555. describe('when the user is not logged in', function () {
  556. beforeEach(function () {
  557. this.req.session = {}
  558. this.AuthenticationController._redirectToLoginOrRegisterPage =
  559. sinon.stub()
  560. this.req.query = {}
  561. this.SessionManager.isUserLoggedIn = sinon.stub().returns(false)
  562. this.middleware(this.req, this.res, this.next)
  563. })
  564. it('should redirect to the register or login page', function () {
  565. this.AuthenticationController._redirectToLoginOrRegisterPage
  566. .calledWith(this.req, this.res)
  567. .should.equal(true)
  568. })
  569. })
  570. })
  571. describe('requireOauth', function () {
  572. beforeEach(function () {
  573. this.res.json = sinon.stub()
  574. this.res.status = sinon.stub().returns(this.res)
  575. this.res.sendStatus = sinon.stub()
  576. this.middleware = this.AuthenticationController.requireOauth('scope')
  577. })
  578. describe('when Oauth2Server authenticates', function () {
  579. beforeEach(function (done) {
  580. this.token = {
  581. accessToken: 'token',
  582. user: 'user',
  583. }
  584. this.Oauth2Server.server.authenticate = sinon
  585. .stub()
  586. .resolves(this.token)
  587. this.middleware(this.req, this.res, () => done())
  588. })
  589. it('should set oauth_token on request', function () {
  590. this.req.oauth_token.should.equal(this.token)
  591. })
  592. it('should set oauth on request', function () {
  593. this.req.oauth.access_token.should.equal(this.token.accessToken)
  594. })
  595. it('should set oauth_user on request', function () {
  596. this.req.oauth_user.should.equal('user')
  597. })
  598. })
  599. describe('when Oauth2Server returns 401 error', function () {
  600. beforeEach(function (done) {
  601. this.res.json.callsFake(() => done())
  602. this.Oauth2Server.server.authenticate.rejects({ code: 401 })
  603. this.middleware(this.req, this.res, this.next)
  604. })
  605. it('should return 401 error', function () {
  606. this.res.status.should.have.been.calledWith(401)
  607. })
  608. it('should not call next', function () {
  609. this.next.should.have.not.been.calledOnce
  610. })
  611. })
  612. })
  613. describe('requireGlobalLogin', function () {
  614. beforeEach(function () {
  615. this.req.headers = {}
  616. this.middleware = sinon.stub()
  617. this.AuthenticationController.requirePrivateApiAuth = sinon
  618. .stub()
  619. .returns(this.middleware)
  620. this.setRedirect = sinon.spy(
  621. this.AuthenticationController,
  622. 'setRedirectInSession'
  623. )
  624. })
  625. afterEach(function () {
  626. this.setRedirect.restore()
  627. })
  628. describe('with white listed url', function () {
  629. beforeEach(function () {
  630. this.AuthenticationController.addEndpointToLoginWhitelist('/login')
  631. this.req._parsedUrl.pathname = '/login'
  632. this.AuthenticationController.requireGlobalLogin(
  633. this.req,
  634. this.res,
  635. this.next
  636. )
  637. })
  638. it('should call next() directly', function () {
  639. this.next.called.should.equal(true)
  640. })
  641. })
  642. describe('with white listed url and a query string', function () {
  643. beforeEach(function () {
  644. this.AuthenticationController.addEndpointToLoginWhitelist('/login')
  645. this.req._parsedUrl.pathname = '/login'
  646. this.req.url = '/login?query=something'
  647. this.AuthenticationController.requireGlobalLogin(
  648. this.req,
  649. this.res,
  650. this.next
  651. )
  652. })
  653. it('should call next() directly', function () {
  654. this.next.called.should.equal(true)
  655. })
  656. })
  657. describe('with http auth', function () {
  658. beforeEach(function () {
  659. this.req.headers.authorization = 'Mock Basic Auth'
  660. this.AuthenticationController.requireGlobalLogin(
  661. this.req,
  662. this.res,
  663. this.next
  664. )
  665. })
  666. it('should pass the request onto requirePrivateApiAuth middleware', function () {
  667. this.middleware
  668. .calledWith(this.req, this.res, this.next)
  669. .should.equal(true)
  670. })
  671. })
  672. describe('with a user session', function () {
  673. beforeEach(function () {
  674. this.req.session = { user: { mock: 'user', _id: 'some_id' } }
  675. this.AuthenticationController.requireGlobalLogin(
  676. this.req,
  677. this.res,
  678. this.next
  679. )
  680. })
  681. it('should call next() directly', function () {
  682. this.next.called.should.equal(true)
  683. })
  684. })
  685. describe('with no login credentials', function () {
  686. beforeEach(function () {
  687. this.req.session = {}
  688. this.SessionManager.isUserLoggedIn = sinon.stub().returns(false)
  689. this.AuthenticationController.requireGlobalLogin(
  690. this.req,
  691. this.res,
  692. this.next
  693. )
  694. })
  695. it('should have called setRedirectInSession', function () {
  696. this.setRedirect.callCount.should.equal(1)
  697. })
  698. it('should redirect to the /login page', function () {
  699. this.res.redirectedTo.should.equal('/login')
  700. })
  701. })
  702. })
  703. describe('requireBasicAuth', function () {
  704. beforeEach(function () {
  705. this.basicAuthUsers = {
  706. 'basic-auth-user': 'basic-auth-password',
  707. }
  708. this.middleware = this.AuthenticationController.requireBasicAuth(
  709. this.basicAuthUsers
  710. )
  711. })
  712. describe('with http auth', function () {
  713. it('should error with incorrect user', function (done) {
  714. this.req.headers = {
  715. authorization: `Basic ${Buffer.from('user:nope').toString('base64')}`,
  716. }
  717. this.req.sendStatus = status => {
  718. expect(status).to.equal(401)
  719. done()
  720. }
  721. this.middleware(this.req, this.req)
  722. })
  723. it('should error with incorrect password', function (done) {
  724. this.req.headers = {
  725. authorization: `Basic ${Buffer.from('basic-auth-user:nope').toString(
  726. 'base64'
  727. )}`,
  728. }
  729. this.req.sendStatus = status => {
  730. expect(status).to.equal(401)
  731. done()
  732. }
  733. this.middleware(this.req, this.req)
  734. })
  735. it('should fail with empty pass', function (done) {
  736. this.req.headers = {
  737. authorization: `Basic ${Buffer.from(`basic-auth-user:`).toString(
  738. 'base64'
  739. )}`,
  740. }
  741. this.req.sendStatus = status => {
  742. expect(status).to.equal(401)
  743. done()
  744. }
  745. this.middleware(this.req, this.req)
  746. })
  747. it('should fail with empty user and password of "undefined"', function (done) {
  748. this.req.headers = {
  749. authorization: `Basic ${Buffer.from(`:undefined`).toString(
  750. 'base64'
  751. )}`,
  752. }
  753. this.req.sendStatus = status => {
  754. expect(status).to.equal(401)
  755. done()
  756. }
  757. this.middleware(this.req, this.req)
  758. })
  759. it('should fail with empty user and empty password', function (done) {
  760. this.req.headers = {
  761. authorization: `Basic ${Buffer.from(`:`).toString('base64')}`,
  762. }
  763. this.req.sendStatus = status => {
  764. expect(status).to.equal(401)
  765. done()
  766. }
  767. this.middleware(this.req, this.req)
  768. })
  769. it('should fail with a user that is not a valid property', function (done) {
  770. this.req.headers = {
  771. authorization: `Basic ${Buffer.from(
  772. `constructor:[Function ]`
  773. ).toString('base64')}`,
  774. }
  775. this.req.sendStatus = status => {
  776. expect(status).to.equal(401)
  777. done()
  778. }
  779. this.middleware(this.req, this.req)
  780. })
  781. it('should succeed with correct user/pass', function (done) {
  782. this.req.headers = {
  783. authorization: `Basic ${Buffer.from(
  784. `basic-auth-user:${this.basicAuthUsers['basic-auth-user']}`
  785. ).toString('base64')}`,
  786. }
  787. this.middleware(this.req, this.res, done)
  788. })
  789. })
  790. })
  791. describe('requirePrivateApiAuth', function () {
  792. beforeEach(function () {
  793. this.AuthenticationController.requireBasicAuth = sinon.stub()
  794. })
  795. it('should call requireBasicAuth with the private API user details', function () {
  796. this.AuthenticationController.requirePrivateApiAuth()
  797. this.AuthenticationController.requireBasicAuth
  798. .calledWith(this.httpAuthUsers)
  799. .should.equal(true)
  800. })
  801. })
  802. describe('_redirectToLoginOrRegisterPage', function () {
  803. beforeEach(function () {
  804. this.middleware = this.AuthenticationController.requireLogin(
  805. (this.options = { load_from_db: false })
  806. )
  807. this.req.session = {}
  808. this.AuthenticationController._redirectToRegisterPage = sinon.stub()
  809. this.AuthenticationController._redirectToLoginPage = sinon.stub()
  810. this.req.query = {}
  811. })
  812. describe('they have come directly to the url', function () {
  813. beforeEach(function () {
  814. this.req.query = {}
  815. this.SessionManager.isUserLoggedIn = sinon.stub().returns(false)
  816. this.middleware(this.req, this.res, this.next)
  817. })
  818. it('should redirect to the login page', function () {
  819. this.AuthenticationController._redirectToRegisterPage
  820. .calledWith(this.req, this.res)
  821. .should.equal(false)
  822. this.AuthenticationController._redirectToLoginPage
  823. .calledWith(this.req, this.res)
  824. .should.equal(true)
  825. })
  826. })
  827. describe('they have come via a templates link', function () {
  828. beforeEach(function () {
  829. this.req.query.zipUrl = 'something'
  830. this.SessionManager.isUserLoggedIn = sinon.stub().returns(false)
  831. this.middleware(this.req, this.res, this.next)
  832. })
  833. it('should redirect to the register page', function () {
  834. this.AuthenticationController._redirectToRegisterPage
  835. .calledWith(this.req, this.res)
  836. .should.equal(true)
  837. this.AuthenticationController._redirectToLoginPage
  838. .calledWith(this.req, this.res)
  839. .should.equal(false)
  840. })
  841. })
  842. describe('they have been invited to a project', function () {
  843. beforeEach(function () {
  844. this.req.session.sharedProjectData = {
  845. project_name: 'something',
  846. user_first_name: 'else',
  847. }
  848. this.SessionManager.isUserLoggedIn = sinon.stub().returns(false)
  849. this.middleware(this.req, this.res, this.next)
  850. })
  851. it('should redirect to the register page', function () {
  852. this.AuthenticationController._redirectToRegisterPage
  853. .calledWith(this.req, this.res)
  854. .should.equal(true)
  855. this.AuthenticationController._redirectToLoginPage
  856. .calledWith(this.req, this.res)
  857. .should.equal(false)
  858. })
  859. })
  860. })
  861. describe('_redirectToRegisterPage', function () {
  862. beforeEach(function () {
  863. this.req.path = '/target/url'
  864. this.req.query = { extra_query: 'foo' }
  865. this.AuthenticationController._redirectToRegisterPage(this.req, this.res)
  866. })
  867. it('should redirect to the register page with a query string attached', function () {
  868. this.req.session.postLoginRedirect.should.equal(
  869. '/target/url?extra_query=foo'
  870. )
  871. this.res.redirectedTo.should.equal('/register?extra_query=foo')
  872. })
  873. it('should log out a message', function () {
  874. this.logger.debug
  875. .calledWith(
  876. { url: this.url },
  877. 'user not logged in so redirecting to register page'
  878. )
  879. .should.equal(true)
  880. })
  881. })
  882. describe('_redirectToLoginPage', function () {
  883. beforeEach(function () {
  884. this.req.path = '/target/url'
  885. this.req.query = { extra_query: 'foo' }
  886. this.AuthenticationController._redirectToLoginPage(this.req, this.res)
  887. })
  888. it('should redirect to the register page with a query string attached', function () {
  889. this.req.session.postLoginRedirect.should.equal(
  890. '/target/url?extra_query=foo'
  891. )
  892. this.res.redirectedTo.should.equal('/login?extra_query=foo')
  893. })
  894. })
  895. describe('_recordSuccessfulLogin', function () {
  896. beforeEach(function () {
  897. this.UserUpdater.updateUser = sinon.stub().yields()
  898. this.AuthenticationController._recordSuccessfulLogin(
  899. this.user._id,
  900. this.callback
  901. )
  902. })
  903. it('should increment the user.login.success metric', function () {
  904. this.Metrics.inc.calledWith('user.login.success').should.equal(true)
  905. })
  906. it("should update the user's login count and last logged in date", function () {
  907. this.UserUpdater.updateUser.args[0][1].$set.lastLoggedIn.should.not.equal(
  908. undefined
  909. )
  910. this.UserUpdater.updateUser.args[0][1].$inc.loginCount.should.equal(1)
  911. })
  912. it('should call the callback', function () {
  913. this.callback.called.should.equal(true)
  914. })
  915. })
  916. describe('_recordFailedLogin', function () {
  917. beforeEach(function () {
  918. this.AuthenticationController._recordFailedLogin(this.callback)
  919. })
  920. it('should increment the user.login.failed metric', function () {
  921. this.Metrics.inc.calledWith('user.login.failed').should.equal(true)
  922. })
  923. it('should call the callback', function () {
  924. this.callback.called.should.equal(true)
  925. })
  926. })
  927. describe('setRedirectInSession', function () {
  928. beforeEach(function () {
  929. this.req = { session: {} }
  930. this.req.path = '/somewhere'
  931. this.req.query = { one: '1' }
  932. })
  933. it('should set redirect property on session', function () {
  934. this.AuthenticationController.setRedirectInSession(this.req)
  935. expect(this.req.session.postLoginRedirect).to.equal('/somewhere?one=1')
  936. })
  937. it('should set the supplied value', function () {
  938. this.AuthenticationController.setRedirectInSession(
  939. this.req,
  940. '/somewhere/specific'
  941. )
  942. expect(this.req.session.postLoginRedirect).to.equal('/somewhere/specific')
  943. })
  944. it('should not allow open redirects', function () {
  945. this.AuthenticationController.setRedirectInSession(
  946. this.req,
  947. 'https://evil.com'
  948. )
  949. expect(this.req.session.postLoginRedirect).to.be.undefined
  950. })
  951. describe('with a png', function () {
  952. beforeEach(function () {
  953. this.req = { session: {} }
  954. })
  955. it('should not set the redirect', function () {
  956. this.AuthenticationController.setRedirectInSession(
  957. this.req,
  958. '/something.png'
  959. )
  960. expect(this.req.session.postLoginRedirect).to.equal(undefined)
  961. })
  962. })
  963. describe('with a js path', function () {
  964. beforeEach(function () {
  965. this.req = { session: {} }
  966. })
  967. it('should not set the redirect', function () {
  968. this.AuthenticationController.setRedirectInSession(
  969. this.req,
  970. '/js/something.js'
  971. )
  972. expect(this.req.session.postLoginRedirect).to.equal(undefined)
  973. })
  974. })
  975. })
  976. describe('getRedirectFromSession', function () {
  977. it('should get redirect property from session', function () {
  978. this.req = { session: { postLoginRedirect: '/a?b=c' } }
  979. expect(
  980. this.AuthenticationController.getRedirectFromSession(this.req)
  981. ).to.equal('/a?b=c')
  982. })
  983. it('should not allow open redirects', function () {
  984. this.req = { session: { postLoginRedirect: 'https://evil.com' } }
  985. expect(this.AuthenticationController.getRedirectFromSession(this.req)).to
  986. .be.null
  987. })
  988. it('handle null values', function () {
  989. this.req = { session: {} }
  990. expect(this.AuthenticationController.getRedirectFromSession(this.req)).to
  991. .be.null
  992. })
  993. })
  994. describe('_clearRedirectFromSession', function () {
  995. beforeEach(function () {
  996. this.req = { session: { postLoginRedirect: '/a?b=c' } }
  997. })
  998. it('should remove the redirect property from session', function () {
  999. this.AuthenticationController._clearRedirectFromSession(this.req)
  1000. expect(this.req.session.postLoginRedirect).to.equal(undefined)
  1001. })
  1002. })
  1003. describe('finishLogin', function () {
  1004. // - get redirect
  1005. // - async handlers
  1006. // - afterLoginSessionSetup
  1007. // - clear redirect
  1008. // - issue redir, two ways
  1009. beforeEach(function () {
  1010. this.AuthenticationController.getRedirectFromSession = sinon
  1011. .stub()
  1012. .returns('/some/page')
  1013. this.req.sessionID = 'thisisacryptographicallysecurerandomid'
  1014. this.req.session = {
  1015. passport: { user: { _id: 'one' } },
  1016. }
  1017. this.req.session.destroy = sinon.stub().yields(null)
  1018. this.req.session.save = sinon.stub().yields(null)
  1019. this.req.sessionStore = { generate: sinon.stub() }
  1020. this.req.login = sinon.stub().yields(null)
  1021. this.AuthenticationController._clearRedirectFromSession = sinon.stub()
  1022. this.AuthenticationController._redirectToReconfirmPage = sinon.stub()
  1023. this.UserSessionsManager.trackSession = sinon.stub()
  1024. this.UserHandler.populateTeamInvites = sinon.stub()
  1025. this.LoginRateLimiter.recordSuccessfulLogin = sinon.stub()
  1026. this.AuthenticationController._recordSuccessfulLogin = sinon.stub()
  1027. this.AnalyticsManager.recordEvent = sinon.stub()
  1028. this.AnalyticsManager.identifyUser = sinon.stub()
  1029. this.acceptsJson.returns(true)
  1030. this.res.json = sinon.stub()
  1031. this.res.redirect = sinon.stub()
  1032. })
  1033. it('should extract the redirect from the session', async function () {
  1034. await this.AuthenticationController.promises.finishLogin(
  1035. this.user,
  1036. this.req,
  1037. this.res,
  1038. this.next
  1039. )
  1040. expect(
  1041. this.AuthenticationController.getRedirectFromSession.callCount
  1042. ).to.equal(1)
  1043. expect(
  1044. this.AuthenticationController.getRedirectFromSession.calledWith(
  1045. this.req
  1046. )
  1047. ).to.equal(true)
  1048. })
  1049. it('should clear redirect from session', async function () {
  1050. await this.AuthenticationController.promises.finishLogin(
  1051. this.user,
  1052. this.req,
  1053. this.res,
  1054. this.next
  1055. )
  1056. expect(
  1057. this.AuthenticationController._clearRedirectFromSession.callCount
  1058. ).to.equal(1)
  1059. expect(
  1060. this.AuthenticationController._clearRedirectFromSession.calledWith(
  1061. this.req
  1062. )
  1063. ).to.equal(true)
  1064. })
  1065. it('should issue a json response with a redirect', async function () {
  1066. await this.AuthenticationController.promises.finishLogin(
  1067. this.user,
  1068. this.req,
  1069. this.res,
  1070. this.next
  1071. )
  1072. expect(
  1073. this.AsyncFormHelper.redirect.calledWith(
  1074. this.req,
  1075. this.res,
  1076. '/some/page'
  1077. )
  1078. ).to.equal(true)
  1079. })
  1080. describe('with a non-json request', function () {
  1081. beforeEach(function () {
  1082. this.acceptsJson.returns(false)
  1083. this.res.json = sinon.stub()
  1084. this.res.redirect = sinon.stub()
  1085. })
  1086. it('should issue a plain redirect', async function () {
  1087. await this.AuthenticationController.promises.finishLogin(
  1088. this.user,
  1089. this.req,
  1090. this.res,
  1091. this.next
  1092. )
  1093. expect(
  1094. this.AsyncFormHelper.redirect.calledWith(
  1095. this.req,
  1096. this.res,
  1097. '/some/page'
  1098. )
  1099. ).to.equal(true)
  1100. })
  1101. })
  1102. describe('when user is flagged to reconfirm', function () {
  1103. beforeEach(function () {
  1104. this.req.session = {}
  1105. this.user.must_reconfirm = true
  1106. })
  1107. it('should redirect to reconfirm page', async function () {
  1108. await this.AuthenticationController.promises.finishLogin(
  1109. this.user,
  1110. this.req,
  1111. this.res,
  1112. this.next
  1113. )
  1114. expect(
  1115. this.AuthenticationController._redirectToReconfirmPage.calledWith(
  1116. this.req
  1117. )
  1118. ).to.equal(true)
  1119. })
  1120. })
  1121. describe('when user account is suspended', function () {
  1122. beforeEach(function () {
  1123. this.req.session = {}
  1124. this.user.suspended = true
  1125. })
  1126. it('should not log in and instead redirect to suspended account page', async function () {
  1127. await this.AuthenticationController.promises.finishLogin(
  1128. this.user,
  1129. this.req,
  1130. this.res,
  1131. this.next
  1132. )
  1133. sinon.assert.notCalled(this.req.login)
  1134. sinon.assert.calledWith(
  1135. this.AsyncFormHelper.redirect,
  1136. this.req,
  1137. this.res,
  1138. '/account-suspended'
  1139. )
  1140. })
  1141. })
  1142. describe('preFinishLogin hook', function () {
  1143. it('call hook and proceed', async function () {
  1144. await this.AuthenticationController.promises.finishLogin(
  1145. this.user,
  1146. this.req,
  1147. this.res,
  1148. this.next
  1149. )
  1150. sinon.assert.calledWith(
  1151. this.Modules.promises.hooks.fire,
  1152. 'preFinishLogin',
  1153. this.req,
  1154. this.res,
  1155. this.user
  1156. )
  1157. expect(this.AsyncFormHelper.redirect.called).to.equal(true)
  1158. })
  1159. it('stop if hook has redirected', async function () {
  1160. this.Modules.promises.hooks.fire = sinon
  1161. .stub()
  1162. .resolves([{ doNotFinish: true }])
  1163. await this.AuthenticationController.promises.finishLogin(
  1164. this.user,
  1165. this.req,
  1166. this.res,
  1167. this.next
  1168. )
  1169. expect(this.next.callCount).to.equal(0)
  1170. expect(this.res.json.callCount).to.equal(0)
  1171. })
  1172. it('call next with hook errors', function (done) {
  1173. this.Modules.promises.hooks.fire = sinon.stub().yields(new Error())
  1174. this.AuthenticationController.promises
  1175. .finishLogin(this.user, this.req, this.res)
  1176. .catch(err => {
  1177. expect(err).to.exist
  1178. expect(this.res.json.callCount).to.equal(0)
  1179. done()
  1180. })
  1181. })
  1182. })
  1183. describe('UserAuditLog', function () {
  1184. it('should add an audit log entry', async function () {
  1185. await this.AuthenticationController.promises.finishLogin(
  1186. this.user,
  1187. this.req,
  1188. this.res,
  1189. this.next
  1190. )
  1191. expect(
  1192. this.UserAuditLogHandler.promises.addEntry
  1193. ).to.have.been.calledWith(
  1194. this.user._id,
  1195. 'login',
  1196. this.user._id,
  1197. '42.42.42.42'
  1198. )
  1199. })
  1200. it('should add an audit log entry before logging the user in', async function () {
  1201. await this.AuthenticationController.promises.finishLogin(
  1202. this.user,
  1203. this.req,
  1204. this.res,
  1205. this.next
  1206. )
  1207. expect(
  1208. this.UserAuditLogHandler.promises.addEntry
  1209. ).to.have.been.calledBefore(this.req.login)
  1210. })
  1211. it('should not log the user in without an audit log entry', function (done) {
  1212. const theError = new Error()
  1213. this.UserAuditLogHandler.promises.addEntry.rejects(theError)
  1214. this.next.callsFake(err => {
  1215. expect(err).to.equal(theError)
  1216. expect(this.next).to.have.been.calledWith(theError)
  1217. expect(this.req.login).to.not.have.been.called
  1218. done()
  1219. })
  1220. this.AuthenticationController.finishLogin(
  1221. this.user,
  1222. this.req,
  1223. this.res,
  1224. this.next
  1225. )
  1226. })
  1227. it('should pass along auditInfo when present', async function () {
  1228. this.AuthenticationController.setAuditInfo(this.req, {
  1229. method: 'Login',
  1230. })
  1231. await this.AuthenticationController.promises.finishLogin(
  1232. this.user,
  1233. this.req,
  1234. this.res
  1235. )
  1236. expect(
  1237. this.UserAuditLogHandler.promises.addEntry
  1238. ).to.have.been.calledWith(
  1239. this.user._id,
  1240. 'login',
  1241. this.user._id,
  1242. '42.42.42.42',
  1243. { method: 'Login' }
  1244. )
  1245. })
  1246. })
  1247. describe('_afterLoginSessionSetup', function () {
  1248. beforeEach(function () {})
  1249. it('should call req.login', async function () {
  1250. await this.AuthenticationController.promises.finishLogin(
  1251. this.user,
  1252. this.req,
  1253. this.res,
  1254. this.next
  1255. )
  1256. this.req.login.callCount.should.equal(1)
  1257. })
  1258. it('should erase the CSRF secret', async function () {
  1259. await this.AuthenticationController.promises.finishLogin(
  1260. this.user,
  1261. this.req,
  1262. this.res,
  1263. this.next
  1264. )
  1265. expect(this.req.session.csrfSecret).to.not.exist
  1266. })
  1267. it('should call req.session.save', async function () {
  1268. await this.AuthenticationController.promises.finishLogin(
  1269. this.user,
  1270. this.req,
  1271. this.res,
  1272. this.next
  1273. )
  1274. this.req.session.save.callCount.should.equal(1)
  1275. })
  1276. it('should call UserSessionsManager.trackSession', async function () {
  1277. await this.AuthenticationController.promises.finishLogin(
  1278. this.user,
  1279. this.req,
  1280. this.res,
  1281. this.next
  1282. )
  1283. this.UserSessionsManager.trackSession.callCount.should.equal(1)
  1284. })
  1285. describe('when req.session.save produces an error', function () {
  1286. beforeEach(function () {
  1287. this.req.session.save = sinon.stub().yields(new Error('woops'))
  1288. })
  1289. it('should produce an error', function (done) {
  1290. this.AuthenticationController.promises
  1291. .finishLogin(this.user, this.req, this.res)
  1292. .catch(err => {
  1293. expect(err).to.not.be.oneOf([null, undefined])
  1294. expect(err).to.be.instanceof(Error)
  1295. done()
  1296. })
  1297. })
  1298. it('should not call UserSessionsManager.trackSession', function (done) {
  1299. this.AuthenticationController.promises
  1300. .finishLogin(this.user, this.req, this.res)
  1301. .catch(err => {
  1302. expect(err).to.exist
  1303. this.UserSessionsManager.trackSession.callCount.should.equal(0)
  1304. done()
  1305. })
  1306. })
  1307. })
  1308. })
  1309. describe('_loginAsyncHandlers', function () {
  1310. beforeEach(async function () {
  1311. await this.AuthenticationController.promises.finishLogin(
  1312. this.user,
  1313. this.req,
  1314. this.res,
  1315. this.next
  1316. )
  1317. })
  1318. it('should call identifyUser', function () {
  1319. sinon.assert.calledWith(
  1320. this.AnalyticsManager.identifyUser,
  1321. this.user._id,
  1322. this.req.session.analyticsId
  1323. )
  1324. })
  1325. it('should setup the user data in the background', function () {
  1326. this.UserHandler.populateTeamInvites
  1327. .calledWith(this.user)
  1328. .should.equal(true)
  1329. })
  1330. it('should set res.session.justLoggedIn', function () {
  1331. this.req.session.justLoggedIn.should.equal(true)
  1332. })
  1333. it('should record the successful login', function () {
  1334. this.AuthenticationController._recordSuccessfulLogin
  1335. .calledWith(this.user._id)
  1336. .should.equal(true)
  1337. })
  1338. it('should tell the rate limiter that there was a success for that email', function () {
  1339. this.LoginRateLimiter.recordSuccessfulLogin
  1340. .calledWith(this.user.email)
  1341. .should.equal(true)
  1342. })
  1343. it('should log the successful login', function () {
  1344. this.logger.debug
  1345. .calledWith(
  1346. { email: this.user.email, userId: this.user._id.toString() },
  1347. 'successful log in'
  1348. )
  1349. .should.equal(true)
  1350. })
  1351. it('should track the login event', function () {
  1352. sinon.assert.calledWith(
  1353. this.AnalyticsManager.recordEventForUserInBackground,
  1354. this.user._id,
  1355. 'user-logged-in'
  1356. )
  1357. })
  1358. })
  1359. })
  1360. })