PasswordResetHandlerTests.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. const SandboxedModule = require('sandboxed-module')
  2. const path = require('path')
  3. const sinon = require('sinon')
  4. const { expect } = require('chai')
  5. const modulePath = path.join(
  6. __dirname,
  7. '../../../../app/src/Features/PasswordReset/PasswordResetHandler'
  8. )
  9. describe('PasswordResetHandler', function () {
  10. beforeEach(function () {
  11. this.settings = { siteUrl: 'https://www.overleaf.com' }
  12. this.OneTimeTokenHandler = {
  13. promises: {
  14. getNewToken: sinon.stub(),
  15. peekValueFromToken: sinon.stub(),
  16. },
  17. peekValueFromToken: sinon.stub(),
  18. expireToken: sinon.stub(),
  19. }
  20. this.UserGetter = {
  21. getUserByMainEmail: sinon.stub(),
  22. getUser: sinon.stub(),
  23. promises: {
  24. getUserByAnyEmail: sinon.stub(),
  25. getUserByMainEmail: sinon.stub(),
  26. },
  27. }
  28. this.EmailHandler = { promises: { sendEmail: sinon.stub() } }
  29. this.AuthenticationManager = {
  30. setUserPasswordInV2: sinon.stub(),
  31. promises: {
  32. setUserPassword: sinon.stub().resolves(),
  33. },
  34. }
  35. this.PasswordResetHandler = SandboxedModule.require(modulePath, {
  36. requires: {
  37. '../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
  38. promises: {
  39. addEntry: sinon.stub().resolves(),
  40. },
  41. }),
  42. '../User/UserGetter': this.UserGetter,
  43. '../Security/OneTimeTokenHandler': this.OneTimeTokenHandler,
  44. '../Email/EmailHandler': this.EmailHandler,
  45. '../Authentication/AuthenticationManager': this.AuthenticationManager,
  46. '@overleaf/settings': this.settings,
  47. '../Authorization/PermissionsManager': (this.PermissionsManager = {
  48. promises: {
  49. checkUserPermissions: sinon.stub(),
  50. },
  51. }),
  52. },
  53. })
  54. this.token = '12312321i'
  55. this.user_id = 'user_id_here'
  56. this.user = { email: (this.email = 'bob@bob.com'), _id: this.user_id }
  57. this.password = 'my great secret password'
  58. this.callback = sinon.stub()
  59. // this should not have any effect now
  60. this.settings.overleaf = true
  61. })
  62. afterEach(function () {
  63. this.settings.overleaf = false
  64. })
  65. describe('generateAndEmailResetToken', function () {
  66. it('should check the user exists', function () {
  67. this.UserGetter.promises.getUserByAnyEmail.resolves()
  68. this.PasswordResetHandler.generateAndEmailResetToken(
  69. this.user.email,
  70. this.callback
  71. )
  72. this.UserGetter.promises.getUserByAnyEmail.should.have.been.calledWith(
  73. this.user.email
  74. )
  75. })
  76. it('should send the email with the token', function (done) {
  77. this.UserGetter.promises.getUserByAnyEmail.resolves(this.user)
  78. this.OneTimeTokenHandler.promises.getNewToken.resolves(this.token)
  79. this.EmailHandler.promises.sendEmail.resolves()
  80. this.PasswordResetHandler.generateAndEmailResetToken(
  81. this.user.email,
  82. (err, status) => {
  83. expect(err).to.not.exist
  84. this.EmailHandler.promises.sendEmail.called.should.equal(true)
  85. status.should.equal('primary')
  86. const args = this.EmailHandler.promises.sendEmail.args[0]
  87. args[0].should.equal('passwordResetRequested')
  88. args[1].setNewPasswordUrl.should.equal(
  89. `${this.settings.siteUrl}/user/password/set?passwordResetToken=${
  90. this.token
  91. }&email=${encodeURIComponent(this.user.email)}`
  92. )
  93. done()
  94. }
  95. )
  96. })
  97. it('should return errors from getUserByAnyEmail', function (done) {
  98. const err = new Error('oops')
  99. this.UserGetter.promises.getUserByAnyEmail.rejects(err)
  100. this.PasswordResetHandler.generateAndEmailResetToken(
  101. this.user.email,
  102. err => {
  103. expect(err).to.equal(err)
  104. done()
  105. }
  106. )
  107. })
  108. describe('when the email exists', function () {
  109. let result
  110. beforeEach(async function () {
  111. this.UserGetter.promises.getUserByAnyEmail.resolves(this.user)
  112. this.OneTimeTokenHandler.promises.getNewToken.resolves(this.token)
  113. this.EmailHandler.promises.sendEmail.resolves()
  114. result =
  115. await this.PasswordResetHandler.promises.generateAndEmailResetToken(
  116. this.email
  117. )
  118. })
  119. it('should set the password token data to the user id and email', function () {
  120. this.OneTimeTokenHandler.promises.getNewToken.should.have.been.calledWith(
  121. 'password',
  122. {
  123. email: this.email,
  124. user_id: this.user._id,
  125. }
  126. )
  127. })
  128. it('should send an email with the token', function () {
  129. this.EmailHandler.promises.sendEmail.called.should.equal(true)
  130. const args = this.EmailHandler.promises.sendEmail.args[0]
  131. args[0].should.equal('passwordResetRequested')
  132. args[1].setNewPasswordUrl.should.equal(
  133. `${this.settings.siteUrl}/user/password/set?passwordResetToken=${
  134. this.token
  135. }&email=${encodeURIComponent(this.user.email)}`
  136. )
  137. })
  138. it('should return status == true', async function () {
  139. expect(result).to.equal('primary')
  140. })
  141. })
  142. describe("when the email doesn't exist", function () {
  143. let result
  144. beforeEach(async function () {
  145. this.UserGetter.promises.getUserByAnyEmail.resolves(null)
  146. result =
  147. await this.PasswordResetHandler.promises.generateAndEmailResetToken(
  148. this.email
  149. )
  150. })
  151. it('should not set the password token data', function () {
  152. this.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false)
  153. })
  154. it('should send an email with the token', function () {
  155. this.EmailHandler.promises.sendEmail.called.should.equal(false)
  156. })
  157. it('should return status == null', function () {
  158. expect(result).to.equal(null)
  159. })
  160. })
  161. describe('when the email is a secondary email', function () {
  162. let result
  163. beforeEach(async function () {
  164. this.UserGetter.promises.getUserByAnyEmail.resolves(this.user)
  165. result =
  166. await this.PasswordResetHandler.promises.generateAndEmailResetToken(
  167. 'secondary@email.com'
  168. )
  169. })
  170. it('should not set the password token data', function () {
  171. this.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false)
  172. })
  173. it('should not send an email with the token', function () {
  174. this.EmailHandler.promises.sendEmail.called.should.equal(false)
  175. })
  176. it('should return status == secondary', function () {
  177. expect(result).to.equal('secondary')
  178. })
  179. })
  180. })
  181. describe('setNewUserPassword', function () {
  182. beforeEach(function () {
  183. this.auditLog = { ip: '0:0:0:0' }
  184. })
  185. describe('when no data is found', function () {
  186. beforeEach(function () {
  187. this.OneTimeTokenHandler.promises.peekValueFromToken.resolves(null)
  188. })
  189. it('should return found == false and reset == false', function () {
  190. this.PasswordResetHandler.setNewUserPassword(
  191. this.token,
  192. this.password,
  193. this.auditLog,
  194. (error, result) => {
  195. expect(error).to.not.exist
  196. expect(result).to.deep.equal({
  197. found: false,
  198. reset: false,
  199. userId: null,
  200. })
  201. }
  202. )
  203. })
  204. })
  205. describe('when the token has a user_id and email', function () {
  206. beforeEach(function () {
  207. this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({
  208. data: {
  209. user_id: this.user._id,
  210. email: this.email,
  211. },
  212. })
  213. this.AuthenticationManager.promises.setUserPassword
  214. .withArgs(this.user, this.password)
  215. .resolves(true)
  216. this.OneTimeTokenHandler.expireToken = sinon
  217. .stub()
  218. .callsArgWith(2, null)
  219. })
  220. describe('when no user is found with this email', function () {
  221. beforeEach(function () {
  222. this.UserGetter.getUserByMainEmail
  223. .withArgs(this.email)
  224. .yields(null, null)
  225. })
  226. it('should return found == false and reset == false', function (done) {
  227. this.PasswordResetHandler.setNewUserPassword(
  228. this.token,
  229. this.password,
  230. this.auditLog,
  231. (err, result) => {
  232. const { found, reset } = result
  233. expect(err).to.not.exist
  234. expect(found).to.be.false
  235. expect(reset).to.be.false
  236. expect(this.OneTimeTokenHandler.expireToken.callCount).to.equal(0)
  237. done()
  238. }
  239. )
  240. })
  241. })
  242. describe("when the email and user don't match", function () {
  243. beforeEach(function () {
  244. this.UserGetter.getUserByMainEmail
  245. .withArgs(this.email)
  246. .yields(null, { _id: 'not-the-same', email: this.email })
  247. this.OneTimeTokenHandler.expireToken.callsArgWith(2, null)
  248. })
  249. it('should return found == false and reset == false', function (done) {
  250. this.PasswordResetHandler.setNewUserPassword(
  251. this.token,
  252. this.password,
  253. this.auditLog,
  254. (err, result) => {
  255. const { found, reset } = result
  256. expect(err).to.not.exist
  257. expect(found).to.be.false
  258. expect(reset).to.be.false
  259. expect(this.OneTimeTokenHandler.expireToken.callCount).to.equal(0)
  260. done()
  261. }
  262. )
  263. })
  264. })
  265. describe('when the email and user match', function () {
  266. describe('success', function () {
  267. beforeEach(function () {
  268. this.UserGetter.promises.getUserByMainEmail.resolves(this.user)
  269. this.OneTimeTokenHandler.expireToken = sinon
  270. .stub()
  271. .callsArgWith(2, null)
  272. })
  273. it('should update the user audit log', function (done) {
  274. this.PasswordResetHandler.setNewUserPassword(
  275. this.token,
  276. this.password,
  277. this.auditLog,
  278. (error, result) => {
  279. sinon.assert.calledWith(
  280. this.UserAuditLogHandler.promises.addEntry,
  281. this.user_id,
  282. 'reset-password',
  283. undefined,
  284. this.auditLog.ip,
  285. { token: this.token.substring(0, 10) }
  286. )
  287. expect(error).to.not.exist
  288. done()
  289. }
  290. )
  291. })
  292. it('should return reset == true and the user id', function (done) {
  293. this.PasswordResetHandler.setNewUserPassword(
  294. this.token,
  295. this.password,
  296. this.auditLog,
  297. (err, result) => {
  298. const { reset, userId } = result
  299. expect(err).to.not.exist
  300. expect(reset).to.be.true
  301. expect(userId).to.equal(this.user._id)
  302. done()
  303. }
  304. )
  305. })
  306. it('should expire the token', function (done) {
  307. this.PasswordResetHandler.setNewUserPassword(
  308. this.token,
  309. this.password,
  310. this.auditLog,
  311. (_err, _result) => {
  312. expect(this.OneTimeTokenHandler.expireToken.called).to.equal(
  313. true
  314. )
  315. done()
  316. }
  317. )
  318. })
  319. describe('when logged in', function () {
  320. beforeEach(function () {
  321. this.auditLog.initiatorId = this.user_id
  322. })
  323. it('should update the user audit log with initiatorId', function (done) {
  324. this.PasswordResetHandler.setNewUserPassword(
  325. this.token,
  326. this.password,
  327. this.auditLog,
  328. (error, result) => {
  329. expect(error).to.not.exist
  330. sinon.assert.calledWith(
  331. this.UserAuditLogHandler.promises.addEntry,
  332. this.user_id,
  333. 'reset-password',
  334. this.user_id,
  335. this.auditLog.ip,
  336. { token: this.token.substring(0, 10) }
  337. )
  338. done()
  339. }
  340. )
  341. })
  342. })
  343. })
  344. describe('errors', function () {
  345. describe('via setUserPassword', function () {
  346. beforeEach(function () {
  347. this.PasswordResetHandler.promises.getUserForPasswordResetToken =
  348. sinon.stub().withArgs(this.token).resolves({ user: this.user })
  349. this.AuthenticationManager.promises.setUserPassword
  350. .withArgs(this.user, this.password)
  351. .rejects()
  352. })
  353. it('should return the error', function (done) {
  354. this.PasswordResetHandler.setNewUserPassword(
  355. this.token,
  356. this.password,
  357. this.auditLog,
  358. (error, _result) => {
  359. expect(error).to.exist
  360. expect(
  361. this.UserAuditLogHandler.promises.addEntry.callCount
  362. ).to.equal(1)
  363. done()
  364. }
  365. )
  366. })
  367. })
  368. describe('via UserAuditLogHandler', function () {
  369. beforeEach(function () {
  370. this.PasswordResetHandler.promises.getUserForPasswordResetToken =
  371. sinon.stub().withArgs(this.token).resolves({ user: this.user })
  372. this.UserAuditLogHandler.promises.addEntry.rejects(
  373. new Error('oops')
  374. )
  375. })
  376. it('should return the error', function (done) {
  377. this.PasswordResetHandler.setNewUserPassword(
  378. this.token,
  379. this.password,
  380. this.auditLog,
  381. (error, _result) => {
  382. expect(error).to.exist
  383. expect(
  384. this.UserAuditLogHandler.promises.addEntry.callCount
  385. ).to.equal(1)
  386. expect(this.AuthenticationManager.promises.setUserPassword).to
  387. .not.have.been.called
  388. done()
  389. }
  390. )
  391. })
  392. })
  393. })
  394. })
  395. })
  396. describe('when the token has a v1_user_id and email', function () {
  397. beforeEach(function () {
  398. this.user.overleaf = { id: 184 }
  399. this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({
  400. data: {
  401. v1_user_id: this.user.overleaf.id,
  402. email: this.email,
  403. },
  404. })
  405. this.AuthenticationManager.promises.setUserPassword
  406. .withArgs(this.user, this.password)
  407. .resolves(true)
  408. this.OneTimeTokenHandler.expireToken = sinon
  409. .stub()
  410. .callsArgWith(2, null)
  411. })
  412. describe('when no user is reset with this email', function () {
  413. beforeEach(function () {
  414. this.UserGetter.getUserByMainEmail
  415. .withArgs(this.email)
  416. .yields(null, null)
  417. })
  418. it('should return reset == false', function (done) {
  419. this.PasswordResetHandler.setNewUserPassword(
  420. this.token,
  421. this.password,
  422. this.auditLog,
  423. (err, result) => {
  424. const { reset } = result
  425. expect(err).to.not.exist
  426. expect(reset).to.be.false
  427. expect(this.OneTimeTokenHandler.expireToken.called).to.equal(
  428. false
  429. )
  430. done()
  431. }
  432. )
  433. })
  434. })
  435. describe("when the email and user don't match", function () {
  436. beforeEach(function () {
  437. this.UserGetter.getUserByMainEmail.withArgs(this.email).yields(null, {
  438. _id: this.user._id,
  439. email: this.email,
  440. overleaf: { id: 'not-the-same' },
  441. })
  442. })
  443. it('should return reset == false', function (done) {
  444. this.PasswordResetHandler.setNewUserPassword(
  445. this.token,
  446. this.password,
  447. this.auditLog,
  448. (err, result) => {
  449. const { reset } = result
  450. expect(err).to.not.exist
  451. expect(reset).to.be.false
  452. expect(this.OneTimeTokenHandler.expireToken.called).to.equal(
  453. false
  454. )
  455. done()
  456. }
  457. )
  458. })
  459. })
  460. describe('when the email and user match', function () {
  461. beforeEach(function () {
  462. this.UserGetter.promises.getUserByMainEmail.resolves(this.user)
  463. })
  464. it('should return reset == true and the user id', function (done) {
  465. this.PasswordResetHandler.setNewUserPassword(
  466. this.token,
  467. this.password,
  468. this.auditLog,
  469. (err, result) => {
  470. const { reset, userId } = result
  471. expect(err).to.not.exist
  472. expect(reset).to.be.true
  473. expect(userId).to.equal(this.user._id)
  474. expect(this.OneTimeTokenHandler.expireToken.called).to.equal(true)
  475. done()
  476. }
  477. )
  478. })
  479. })
  480. })
  481. })
  482. describe('getUserForPasswordResetToken', function () {
  483. beforeEach(function () {
  484. this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({
  485. data: {
  486. user_id: this.user._id,
  487. email: this.email,
  488. },
  489. remainingPeeks: 1,
  490. })
  491. this.UserGetter.promises.getUserByMainEmail.resolves({
  492. _id: this.user._id,
  493. email: this.email,
  494. })
  495. })
  496. it('should returns errors from user permissions', async function () {
  497. let error
  498. const err = new Error('nope')
  499. this.PermissionsManager.promises.checkUserPermissions.rejects(err)
  500. try {
  501. await this.PasswordResetHandler.promises.getUserForPasswordResetToken(
  502. 'abc123'
  503. )
  504. } catch (e) {
  505. error = e
  506. }
  507. expect(error).to.deep.equal(error)
  508. })
  509. it('returns user when user has permissions and remaining peaks', async function () {
  510. const result =
  511. await this.PasswordResetHandler.promises.getUserForPasswordResetToken(
  512. 'abc123'
  513. )
  514. expect(result).to.deep.equal({
  515. user: { _id: this.user._id, email: this.email },
  516. remainingPeeks: 1,
  517. })
  518. })
  519. })
  520. })