AuthenticationControllerTests.js 38 KB

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