SessionTests.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  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.js'
  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. })
  65. describe('two sessions', function () {
  66. beforeEach(function () {
  67. // set up second session for this user
  68. this.user2 = new UserHelper()
  69. this.user2.email = this.user1.email
  70. this.user2.emails = this.user1.emails
  71. this.user2.password = this.user1.password
  72. })
  73. it('should have two sessions in UserSessions set', function (done) {
  74. async.series(
  75. [
  76. next => {
  77. redis.clearUserSessions(this.user1, next)
  78. },
  79. // login, should add session to set
  80. next => {
  81. this.user1.login(err => next(err))
  82. },
  83. next => {
  84. redis.getUserSessions(this.user1, (err, sessions) => {
  85. expect(err).to.not.exist
  86. expect(sessions.length).to.equal(1)
  87. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  88. next()
  89. })
  90. },
  91. // login again, should add the second session to set
  92. next => {
  93. this.user2.login(err => next(err))
  94. },
  95. next => {
  96. redis.getUserSessions(this.user1, (err, sessions) => {
  97. expect(err).to.not.exist
  98. expect(sessions.length).to.equal(2)
  99. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  100. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  101. next()
  102. })
  103. },
  104. // both should be able to access project list page
  105. next => {
  106. this.user1.getProjectListPage((err, statusCode) => {
  107. expect(err).to.equal(null)
  108. expect(statusCode).to.equal(200)
  109. next()
  110. })
  111. },
  112. next => {
  113. this.user2.getProjectListPage((err, statusCode) => {
  114. expect(err).to.equal(null)
  115. expect(statusCode).to.equal(200)
  116. next()
  117. })
  118. },
  119. // logout first session, should remove session from set
  120. next => {
  121. this.user1.logout(err => next(err))
  122. },
  123. next => {
  124. redis.getUserSessions(this.user1, (err, sessions) => {
  125. expect(err).to.not.exist
  126. expect(sessions.length).to.equal(1)
  127. next()
  128. })
  129. },
  130. // first session should not have access to project list page
  131. next => {
  132. this.user1.getProjectListPage((err, statusCode) => {
  133. expect(err).to.equal(null)
  134. expect(statusCode).to.equal(302)
  135. next()
  136. })
  137. },
  138. // second session should still have access to settings
  139. next => {
  140. this.user2.getProjectListPage((err, statusCode) => {
  141. expect(err).to.equal(null)
  142. expect(statusCode).to.equal(200)
  143. next()
  144. })
  145. },
  146. // logout second session, should remove last session from set
  147. next => {
  148. this.user2.logout(err => next(err))
  149. },
  150. next => {
  151. redis.getUserSessions(this.user1, (err, sessions) => {
  152. expect(err).to.not.exist
  153. expect(sessions.length).to.equal(0)
  154. next()
  155. })
  156. },
  157. // second session should not have access to project list page
  158. next => {
  159. this.user2.getProjectListPage((err, statusCode) => {
  160. expect(err).to.equal(null)
  161. expect(statusCode).to.equal(302)
  162. next()
  163. })
  164. },
  165. ],
  166. (err, result) => {
  167. if (err) {
  168. throw err
  169. }
  170. done()
  171. }
  172. )
  173. })
  174. })
  175. describe('three sessions, password reset', function () {
  176. beforeEach(function () {
  177. // set up second session for this user
  178. this.user2 = new UserHelper()
  179. this.user2.email = this.user1.email
  180. this.user2.emails = this.user1.emails
  181. this.user2.password = this.user1.password
  182. this.user3 = new UserHelper()
  183. this.user3.email = this.user1.email
  184. this.user3.emails = this.user1.emails
  185. this.user3.password = this.user1.password
  186. })
  187. it('should erase both sessions when password is reset', function (done) {
  188. async.series(
  189. [
  190. next => {
  191. redis.clearUserSessions(this.user1, next)
  192. },
  193. // login, should add session to set
  194. next => {
  195. this.user1.login(err => next(err))
  196. },
  197. next => {
  198. redis.getUserSessions(this.user1, (err, sessions) => {
  199. expect(err).to.not.exist
  200. expect(sessions.length).to.equal(1)
  201. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  202. next()
  203. })
  204. },
  205. // login again, should add the second session to set
  206. next => {
  207. this.user2.login(err => next(err))
  208. },
  209. next => {
  210. redis.getUserSessions(this.user1, (err, sessions) => {
  211. expect(err).to.not.exist
  212. expect(sessions.length).to.equal(2)
  213. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  214. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  215. next()
  216. })
  217. },
  218. // login third session, should add the second session to set
  219. next => {
  220. this.user3.login(err => next(err))
  221. },
  222. next => {
  223. redis.getUserSessions(this.user1, (err, sessions) => {
  224. expect(err).to.not.exist
  225. expect(sessions.length).to.equal(3)
  226. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  227. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  228. next()
  229. })
  230. },
  231. // password reset from second session, should erase two of the three sessions
  232. next => {
  233. this.user2.changePassword(`password${Date.now()}`, err => next(err))
  234. },
  235. next => {
  236. redis.getUserSessions(this.user2, (err, sessions) => {
  237. expect(err).to.not.exist
  238. expect(sessions.length).to.equal(1)
  239. next()
  240. })
  241. },
  242. // users one and three should not be able to access project list page
  243. next => {
  244. this.user1.getProjectListPage((err, statusCode) => {
  245. expect(err).to.equal(null)
  246. expect(statusCode).to.equal(302)
  247. next()
  248. })
  249. },
  250. next => {
  251. this.user3.getProjectListPage((err, statusCode) => {
  252. expect(err).to.equal(null)
  253. expect(statusCode).to.equal(302)
  254. next()
  255. })
  256. },
  257. // user two should still be logged in, and able to access project list page
  258. next => {
  259. this.user2.getProjectListPage((err, statusCode) => {
  260. expect(err).to.equal(null)
  261. expect(statusCode).to.equal(200)
  262. next()
  263. })
  264. },
  265. // logout second session, should remove last session from set
  266. next => {
  267. this.user2.logout(err => next(err))
  268. },
  269. next => {
  270. redis.getUserSessions(this.user1, (err, sessions) => {
  271. expect(err).to.not.exist
  272. expect(sessions.length).to.equal(0)
  273. next()
  274. })
  275. },
  276. ],
  277. (err, result) => {
  278. if (err) {
  279. throw err
  280. }
  281. done()
  282. }
  283. )
  284. })
  285. })
  286. describe('three sessions, sessions page', function () {
  287. beforeEach(function (done) {
  288. // set up second session for this user
  289. this.user2 = new UserHelper()
  290. this.user2.email = this.user1.email
  291. this.user2.emails = this.user1.emails
  292. this.user2.password = this.user1.password
  293. this.user3 = new UserHelper()
  294. this.user3.email = this.user1.email
  295. this.user3.emails = this.user1.emails
  296. this.user3.password = this.user1.password
  297. async.series([this.user2.login.bind(this.user2)], done)
  298. })
  299. it('should allow the user to erase the other two sessions', function (done) {
  300. async.series(
  301. [
  302. next => {
  303. redis.clearUserSessions(this.user1, next)
  304. },
  305. // login, should add session to set
  306. next => {
  307. this.user1.login(err => next(err))
  308. },
  309. next => {
  310. redis.getUserSessions(this.user1, (err, sessions) => {
  311. expect(err).to.not.exist
  312. expect(sessions.length).to.equal(1)
  313. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  314. next()
  315. })
  316. },
  317. // login again, should add the second session to set
  318. next => {
  319. this.user2.login(err => next(err))
  320. },
  321. next => {
  322. redis.getUserSessions(this.user1, (err, sessions) => {
  323. expect(err).to.not.exist
  324. expect(sessions.length).to.equal(2)
  325. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  326. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  327. next()
  328. })
  329. },
  330. // login third session, should add the second session to set
  331. next => {
  332. this.user3.login(err => next(err))
  333. },
  334. next => {
  335. redis.getUserSessions(this.user1, (err, sessions) => {
  336. expect(err).to.not.exist
  337. expect(sessions.length).to.equal(3)
  338. expect(sessions[0].slice(0, 5)).to.equal('sess:')
  339. expect(sessions[1].slice(0, 5)).to.equal('sess:')
  340. next()
  341. })
  342. },
  343. // check the sessions page
  344. next => {
  345. this.user2.request.get(
  346. {
  347. uri: '/user/sessions',
  348. },
  349. (err, response, body) => {
  350. expect(err).to.be.oneOf([null, undefined])
  351. expect(response.statusCode).to.equal(200)
  352. next()
  353. }
  354. )
  355. },
  356. // clear sessions from second session, should erase two of the three sessions
  357. next => {
  358. this.user2.getCsrfToken(err => {
  359. expect(err).to.be.oneOf([null, undefined])
  360. this.user2.request.post(
  361. {
  362. uri: '/user/sessions/clear',
  363. },
  364. err => next(err)
  365. )
  366. })
  367. },
  368. next => {
  369. redis.getUserSessions(this.user2, (err, sessions) => {
  370. expect(err).to.not.exist
  371. expect(sessions.length).to.equal(1)
  372. next()
  373. })
  374. },
  375. // users one and three should not be able to access project list page
  376. next => {
  377. this.user1.getProjectListPage((err, statusCode) => {
  378. expect(err).to.equal(null)
  379. expect(statusCode).to.equal(302)
  380. next()
  381. })
  382. },
  383. next => {
  384. this.user3.getProjectListPage((err, statusCode) => {
  385. expect(err).to.equal(null)
  386. expect(statusCode).to.equal(302)
  387. next()
  388. })
  389. },
  390. // user two should still be logged in, and able to access project list page
  391. next => {
  392. this.user2.getProjectListPage((err, statusCode) => {
  393. expect(err).to.equal(null)
  394. expect(statusCode).to.equal(200)
  395. next()
  396. })
  397. },
  398. // logout second session, should remove last session from set
  399. next => {
  400. this.user2.logout(err => next(err))
  401. },
  402. next => {
  403. redis.getUserSessions(this.user1, (err, sessions) => {
  404. expect(err).to.not.exist
  405. expect(sessions.length).to.equal(0)
  406. next()
  407. })
  408. },
  409. // the user audit log should have been updated
  410. next => {
  411. this.user1.getAuditLogWithoutNoise((error, auditLog) => {
  412. expect(error).not.to.exist
  413. expect(auditLog).to.exist
  414. expect(auditLog[0].operation).to.equal('clear-sessions')
  415. expect(auditLog[0].ipAddress).to.exist
  416. expect(auditLog[0].initiatorId).to.exist
  417. expect(auditLog[0].timestamp).to.exist
  418. expect(auditLog[0].info.sessions.length).to.equal(2)
  419. next()
  420. })
  421. },
  422. ],
  423. (err, result) => {
  424. if (err) {
  425. throw err
  426. }
  427. done()
  428. }
  429. )
  430. })
  431. })
  432. describe('validationToken', function () {
  433. const User = UserHelper.promises
  434. async function tryWithValidationToken(validationToken) {
  435. const user = new User()
  436. await user.login()
  437. await checkSessionIsValid(user)
  438. const [, sid] = user.sessionCookie().value.match(/^s:(.+?)\./)
  439. const key = `sess:${sid}`
  440. const sess = JSON.parse(await rclient.get(key))
  441. expect(sess.validationToken).to.equal('v1:' + sid.slice(-4))
  442. sess.validationToken = validationToken
  443. await rclient.set(key, JSON.stringify(sess))
  444. {
  445. // The current code destroys the session and throws an error/500.
  446. // Check for login redirect on page reload.
  447. await user.doRequest('GET', '/project')
  448. const { response } = await user.doRequest('GET', '/project')
  449. expect(response.statusCode).to.equal(302)
  450. expect(response.headers.location).to.equal('/login?')
  451. }
  452. }
  453. async function getOtherUsersValidationToken() {
  454. const otherUser = new User()
  455. await otherUser.login()
  456. await checkSessionIsValid(otherUser)
  457. const { validationToken } = await otherUser.getSession()
  458. expect(validationToken).to.match(/^v1:.{4}$/)
  459. return validationToken
  460. }
  461. async function checkSessionIsValid(user) {
  462. const { response } = await user.doRequest('GET', '/project')
  463. expect(response.statusCode).to.equal(200)
  464. }
  465. it('should reject the redis value when missing', async function () {
  466. await tryWithValidationToken(undefined)
  467. })
  468. it('should reject the redis value when empty', async function () {
  469. await tryWithValidationToken('')
  470. })
  471. it('should reject the redis value when out of sync', async function () {
  472. await tryWithValidationToken(await getOtherUsersValidationToken())
  473. })
  474. it('should ignore overwrites in app code', async function () {
  475. const otherUsersValidationToken = await getOtherUsersValidationToken()
  476. const user = new User()
  477. await user.login()
  478. await checkSessionIsValid(user)
  479. const { validationToken: token1 } = await user.getSession()
  480. const allowedUpdateValue = 'allowed-update-value'
  481. await user.setInSession({
  482. validationToken: otherUsersValidationToken,
  483. // also update another field to check that the write operation went through
  484. allowedUpdate: allowedUpdateValue,
  485. })
  486. const { validationToken: token2, allowedUpdate } = await user.getSession()
  487. expect(allowedUpdate).to.equal(allowedUpdateValue)
  488. expect(token1).to.equal(token2)
  489. await checkSessionIsValid(user)
  490. })
  491. })
  492. })