SessionTests.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. import { expect } from 'chai'
  2. import async from 'async'
  3. import UserHelper from './helpers/User.mjs'
  4. import redis from './helpers/redis.mjs'
  5. import UserSessionsRedis from '../../../app/src/Features/User/UserSessionsRedis.mjs'
  6. const rclient = UserSessionsRedis.client()
  7. describe('Sessions', function () {
  8. beforeEach(function (done) {
  9. this.timeout(20000)
  10. this.user1 = new UserHelper()
  11. this.site_admin = new UserHelper({ email: 'admin@example.com' })
  12. async.series(
  13. [cb => this.user1.login(cb), cb => this.user1.logout(cb)],
  14. done
  15. )
  16. })
  17. describe('one session', function () {
  18. it('should have one session in UserSessions set', function (done) {
  19. async.series(
  20. [
  21. next => {
  22. redis.clearUserSessions(this.user1, next)
  23. },
  24. // login, should add session to set
  25. next => {
  26. this.user1.login(err => next(err))
  27. },
  28. next => {
  29. redis.getUserSessions(this.user1, (err, sessions) => {
  30. expect(err).to.not.exist
  31. expect(sessions.length).to.equal(1)
  32. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  33. next()
  34. })
  35. },
  36. // should be able to access project list page
  37. next => {
  38. this.user1.getProjectListPage((err, statusCode) => {
  39. expect(err).to.equal(null)
  40. expect(statusCode).to.equal(200)
  41. next()
  42. })
  43. },
  44. // logout, should remove session from set
  45. next => {
  46. this.user1.logout(err => next(err))
  47. },
  48. next => {
  49. redis.getUserSessions(this.user1, (err, sessions) => {
  50. expect(err).to.not.exist
  51. expect(sessions.length).to.equal(0)
  52. next()
  53. })
  54. },
  55. ],
  56. (err, result) => {
  57. if (err) {
  58. throw err
  59. }
  60. done()
  61. }
  62. )
  63. })
  64. it('should update audit log on logout', function (done) {
  65. async.series(
  66. [
  67. next => {
  68. redis.clearUserSessions(this.user1, next)
  69. },
  70. // login
  71. next => {
  72. this.user1.login(err => next(err))
  73. },
  74. // logout, should add logout audit log entry (happens in background)
  75. next => {
  76. this.user1.logout(err => next(err))
  77. },
  78. // poll for audit log entry since it's written in the background
  79. next => {
  80. let attempts = 0
  81. const checkAuditLog = () => {
  82. this.user1.getAuditLogWithoutNoise((error, auditLog) => {
  83. if (error) return next(error)
  84. const logoutEntries = auditLog.filter(
  85. entry => entry.operation === 'logout'
  86. )
  87. // If we found the logout entry, we're done
  88. if (logoutEntries.length > 0) {
  89. expect(logoutEntries.length).to.be.greaterThan(0)
  90. const lastLogout = logoutEntries[logoutEntries.length - 1]
  91. expect(lastLogout.operation).to.equal('logout')
  92. expect(lastLogout.ipAddress).to.exist
  93. expect(lastLogout.initiatorId).to.exist
  94. expect(lastLogout.timestamp).to.exist
  95. return next()
  96. }
  97. // Otherwise retry up to 10 times
  98. attempts++
  99. if (attempts >= 10) {
  100. return next(
  101. new Error(
  102. 'Logout audit log entry not found after 10 attempts'
  103. )
  104. )
  105. }
  106. setTimeout(checkAuditLog, 25)
  107. })
  108. }
  109. checkAuditLog()
  110. },
  111. ],
  112. (err, result) => {
  113. if (err) {
  114. throw err
  115. }
  116. done()
  117. }
  118. )
  119. })
  120. })
  121. describe('two sessions', function () {
  122. beforeEach(function () {
  123. // set up second session for this user
  124. this.user2 = new UserHelper()
  125. this.user2.email = this.user1.email
  126. this.user2.emails = this.user1.emails
  127. this.user2.password = this.user1.password
  128. })
  129. it('should have two sessions in UserSessions set', function (done) {
  130. async.series(
  131. [
  132. next => {
  133. redis.clearUserSessions(this.user1, next)
  134. },
  135. // login, should add session to set
  136. next => {
  137. this.user1.login(err => next(err))
  138. },
  139. next => {
  140. redis.getUserSessions(this.user1, (err, sessions) => {
  141. expect(err).to.not.exist
  142. expect(sessions.length).to.equal(1)
  143. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  144. next()
  145. })
  146. },
  147. // login again, should add the second session to set
  148. next => {
  149. this.user2.login(err => next(err))
  150. },
  151. next => {
  152. redis.getUserSessions(this.user1, (err, sessions) => {
  153. expect(err).to.not.exist
  154. expect(sessions.length).to.equal(2)
  155. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  156. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  157. next()
  158. })
  159. },
  160. // both should be able to access project list page
  161. next => {
  162. this.user1.getProjectListPage((err, statusCode) => {
  163. expect(err).to.equal(null)
  164. expect(statusCode).to.equal(200)
  165. next()
  166. })
  167. },
  168. next => {
  169. this.user2.getProjectListPage((err, statusCode) => {
  170. expect(err).to.equal(null)
  171. expect(statusCode).to.equal(200)
  172. next()
  173. })
  174. },
  175. // logout first session, should remove session from set
  176. next => {
  177. this.user1.logout(err => next(err))
  178. },
  179. next => {
  180. redis.getUserSessions(this.user1, (err, sessions) => {
  181. expect(err).to.not.exist
  182. expect(sessions.length).to.equal(1)
  183. next()
  184. })
  185. },
  186. // first session should not have access to project list page
  187. next => {
  188. this.user1.getProjectListPage((err, statusCode) => {
  189. expect(err).to.equal(null)
  190. expect(statusCode).to.equal(302)
  191. next()
  192. })
  193. },
  194. // second session should still have access to settings
  195. next => {
  196. this.user2.getProjectListPage((err, statusCode) => {
  197. expect(err).to.equal(null)
  198. expect(statusCode).to.equal(200)
  199. next()
  200. })
  201. },
  202. // logout second session, should remove last session from set
  203. next => {
  204. this.user2.logout(err => next(err))
  205. },
  206. next => {
  207. redis.getUserSessions(this.user1, (err, sessions) => {
  208. expect(err).to.not.exist
  209. expect(sessions.length).to.equal(0)
  210. next()
  211. })
  212. },
  213. // second session should not have access to project list page
  214. next => {
  215. this.user2.getProjectListPage((err, statusCode) => {
  216. expect(err).to.equal(null)
  217. expect(statusCode).to.equal(302)
  218. next()
  219. })
  220. },
  221. ],
  222. (err, result) => {
  223. if (err) {
  224. throw err
  225. }
  226. done()
  227. }
  228. )
  229. })
  230. })
  231. describe('three sessions, password reset', function () {
  232. beforeEach(function () {
  233. // set up second session for this user
  234. this.user2 = new UserHelper()
  235. this.user2.email = this.user1.email
  236. this.user2.emails = this.user1.emails
  237. this.user2.password = this.user1.password
  238. this.user3 = new UserHelper()
  239. this.user3.email = this.user1.email
  240. this.user3.emails = this.user1.emails
  241. this.user3.password = this.user1.password
  242. })
  243. it('should erase both sessions when password is reset', function (done) {
  244. async.series(
  245. [
  246. next => {
  247. redis.clearUserSessions(this.user1, next)
  248. },
  249. // login, should add session to set
  250. next => {
  251. this.user1.login(err => next(err))
  252. },
  253. next => {
  254. redis.getUserSessions(this.user1, (err, sessions) => {
  255. expect(err).to.not.exist
  256. expect(sessions.length).to.equal(1)
  257. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  258. next()
  259. })
  260. },
  261. // login again, should add the second session to set
  262. next => {
  263. this.user2.login(err => next(err))
  264. },
  265. next => {
  266. redis.getUserSessions(this.user1, (err, sessions) => {
  267. expect(err).to.not.exist
  268. expect(sessions.length).to.equal(2)
  269. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  270. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  271. next()
  272. })
  273. },
  274. // login third session, should add the second session to set
  275. next => {
  276. this.user3.login(err => next(err))
  277. },
  278. next => {
  279. redis.getUserSessions(this.user1, (err, sessions) => {
  280. expect(err).to.not.exist
  281. expect(sessions.length).to.equal(3)
  282. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  283. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  284. next()
  285. })
  286. },
  287. // password reset from second session, should erase two of the three sessions
  288. next => {
  289. this.user2.changePassword(`password${Date.now()}`, err => next(err))
  290. },
  291. next => {
  292. redis.getUserSessions(this.user2, (err, sessions) => {
  293. expect(err).to.not.exist
  294. expect(sessions.length).to.equal(1)
  295. next()
  296. })
  297. },
  298. // users one and three should not be able to access project list page
  299. next => {
  300. this.user1.getProjectListPage((err, statusCode) => {
  301. expect(err).to.equal(null)
  302. expect(statusCode).to.equal(302)
  303. next()
  304. })
  305. },
  306. next => {
  307. this.user3.getProjectListPage((err, statusCode) => {
  308. expect(err).to.equal(null)
  309. expect(statusCode).to.equal(302)
  310. next()
  311. })
  312. },
  313. // user two should still be logged in, and able to access project list page
  314. next => {
  315. this.user2.getProjectListPage((err, statusCode) => {
  316. expect(err).to.equal(null)
  317. expect(statusCode).to.equal(200)
  318. next()
  319. })
  320. },
  321. // logout second session, should remove last session from set
  322. next => {
  323. this.user2.logout(err => next(err))
  324. },
  325. next => {
  326. redis.getUserSessions(this.user1, (err, sessions) => {
  327. expect(err).to.not.exist
  328. expect(sessions.length).to.equal(0)
  329. next()
  330. })
  331. },
  332. ],
  333. (err, result) => {
  334. if (err) {
  335. throw err
  336. }
  337. done()
  338. }
  339. )
  340. })
  341. })
  342. describe('three sessions, sessions page', function () {
  343. beforeEach(function (done) {
  344. // set up second session for this user
  345. this.user2 = new UserHelper()
  346. this.user2.email = this.user1.email
  347. this.user2.emails = this.user1.emails
  348. this.user2.password = this.user1.password
  349. this.user3 = new UserHelper()
  350. this.user3.email = this.user1.email
  351. this.user3.emails = this.user1.emails
  352. this.user3.password = this.user1.password
  353. async.series([this.user2.login.bind(this.user2)], done)
  354. })
  355. it('should allow the user to erase the other two sessions', function (done) {
  356. async.series(
  357. [
  358. next => {
  359. redis.clearUserSessions(this.user1, next)
  360. },
  361. // login, should add session to set
  362. next => {
  363. this.user1.login(err => next(err))
  364. },
  365. next => {
  366. redis.getUserSessions(this.user1, (err, sessions) => {
  367. expect(err).to.not.exist
  368. expect(sessions.length).to.equal(1)
  369. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  370. next()
  371. })
  372. },
  373. // login again, should add the second session to set
  374. next => {
  375. this.user2.login(err => next(err))
  376. },
  377. next => {
  378. redis.getUserSessions(this.user1, (err, sessions) => {
  379. expect(err).to.not.exist
  380. expect(sessions.length).to.equal(2)
  381. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  382. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  383. next()
  384. })
  385. },
  386. // login third session, should add the second session to set
  387. next => {
  388. this.user3.login(err => next(err))
  389. },
  390. next => {
  391. redis.getUserSessions(this.user1, (err, sessions) => {
  392. expect(err).to.not.exist
  393. expect(sessions.length).to.equal(3)
  394. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  395. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  396. next()
  397. })
  398. },
  399. // check the sessions page
  400. next => {
  401. this.user2.request.get(
  402. {
  403. uri: '/user/sessions',
  404. },
  405. (err, response, body) => {
  406. expect(err).to.be.oneOf([null, undefined])
  407. expect(response.statusCode).to.equal(200)
  408. next()
  409. }
  410. )
  411. },
  412. // clear sessions from second session, should erase two of the three sessions
  413. next => {
  414. this.user2.getCsrfToken(err => {
  415. expect(err).to.be.oneOf([null, undefined])
  416. this.user2.request.post(
  417. {
  418. uri: '/user/sessions/clear',
  419. },
  420. err => next(err)
  421. )
  422. })
  423. },
  424. next => {
  425. redis.getUserSessions(this.user2, (err, sessions) => {
  426. expect(err).to.not.exist
  427. expect(sessions.length).to.equal(1)
  428. next()
  429. })
  430. },
  431. // users one and three should not be able to access project list page
  432. next => {
  433. this.user1.getProjectListPage((err, statusCode) => {
  434. expect(err).to.equal(null)
  435. expect(statusCode).to.equal(302)
  436. next()
  437. })
  438. },
  439. next => {
  440. this.user3.getProjectListPage((err, statusCode) => {
  441. expect(err).to.equal(null)
  442. expect(statusCode).to.equal(302)
  443. next()
  444. })
  445. },
  446. // user two should still be logged in, and able to access project list page
  447. next => {
  448. this.user2.getProjectListPage((err, statusCode) => {
  449. expect(err).to.equal(null)
  450. expect(statusCode).to.equal(200)
  451. next()
  452. })
  453. },
  454. // logout second session, should remove last session from set
  455. next => {
  456. this.user2.logout(err => next(err))
  457. },
  458. next => {
  459. redis.getUserSessions(this.user1, (err, sessions) => {
  460. expect(err).to.not.exist
  461. expect(sessions.length).to.equal(0)
  462. next()
  463. })
  464. },
  465. // the user audit log should have been updated
  466. next => {
  467. this.user1.getAuditLogWithoutNoise((error, auditLog) => {
  468. expect(error).not.to.exist
  469. expect(auditLog).to.exist
  470. // find the clear-sessions entry
  471. const clearSessionsEntries = auditLog.filter(
  472. entry => entry.operation === 'clear-sessions'
  473. )
  474. expect(clearSessionsEntries.length).to.equal(1)
  475. expect(clearSessionsEntries[0].operation).to.equal(
  476. 'clear-sessions'
  477. )
  478. expect(clearSessionsEntries[0].ipAddress).to.exist
  479. expect(clearSessionsEntries[0].initiatorId).to.exist
  480. expect(clearSessionsEntries[0].timestamp).to.exist
  481. expect(clearSessionsEntries[0].info.sessions.length).to.equal(2)
  482. next()
  483. })
  484. },
  485. ],
  486. (err, result) => {
  487. if (err) {
  488. throw err
  489. }
  490. done()
  491. }
  492. )
  493. })
  494. })
  495. describe('validationToken', function () {
  496. const User = UserHelper.promises
  497. async function tryWithValidationToken(validationToken) {
  498. const user = new User()
  499. await user.login()
  500. await checkSessionIsValid(user)
  501. const [, sid] = user.sessionCookie().value.match(/^s:(.+?)\./)
  502. const key = `sess:${sid}`
  503. const sess = JSON.parse(await rclient.get(key))
  504. expect(sess.validationToken).to.equal('v1:' + sid.slice(-4))
  505. sess.validationToken = validationToken
  506. await rclient.set(key, JSON.stringify(sess))
  507. {
  508. // The current code destroys the session and throws an error/500.
  509. // Check for login redirect on page reload.
  510. await user.doRequest('GET', '/project')
  511. const { response } = await user.doRequest('GET', '/project')
  512. expect(response.statusCode).to.equal(302)
  513. expect(response.headers.location).to.equal('/login?')
  514. }
  515. }
  516. async function getOtherUsersValidationToken() {
  517. const otherUser = new User()
  518. await otherUser.login()
  519. await checkSessionIsValid(otherUser)
  520. const { validationToken } = await otherUser.getSession()
  521. expect(validationToken).to.match(/^v1:.{4}$/)
  522. return validationToken
  523. }
  524. async function checkSessionIsValid(user) {
  525. const { response } = await user.doRequest('GET', '/project')
  526. expect(response.statusCode).to.equal(200)
  527. }
  528. it('should reject the redis value when missing', async function () {
  529. await tryWithValidationToken(undefined)
  530. })
  531. it('should reject the redis value when empty', async function () {
  532. await tryWithValidationToken('')
  533. })
  534. it('should reject the redis value when out of sync', async function () {
  535. await tryWithValidationToken(await getOtherUsersValidationToken())
  536. })
  537. it('should ignore overwrites in app code', async function () {
  538. const otherUsersValidationToken = await getOtherUsersValidationToken()
  539. const user = new User()
  540. await user.login()
  541. await checkSessionIsValid(user)
  542. const { validationToken: token1 } = await user.getSession()
  543. const allowedUpdateValue = 'allowed-update-value'
  544. await user.setInSession({
  545. validationToken: otherUsersValidationToken,
  546. // also update another field to check that the write operation went through
  547. allowedUpdate: allowedUpdateValue,
  548. })
  549. const { validationToken: token2, allowedUpdate } = await user.getSession()
  550. expect(allowedUpdate).to.equal(allowedUpdateValue)
  551. expect(token1).to.equal(token2)
  552. await checkSessionIsValid(user)
  553. })
  554. })
  555. })