AuthenticationControllerTests.js 41 KB

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