AuthenticationControllerTests.js 41 KB

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