AuthenticationManagerTests.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const SandboxedModule = require('sandboxed-module')
  4. const { ObjectId } = require('mongodb')
  5. const AuthenticationErrors = require('../../../../app/src/Features/Authentication/AuthenticationErrors')
  6. const tk = require('timekeeper')
  7. const modulePath =
  8. '../../../../app/src/Features/Authentication/AuthenticationManager.js'
  9. describe('AuthenticationManager', function () {
  10. beforeEach(function () {
  11. tk.freeze(Date.now())
  12. this.settings = { security: { bcryptRounds: 4 } }
  13. this.AuthenticationManager = SandboxedModule.require(modulePath, {
  14. requires: {
  15. '../../models/User': {
  16. User: (this.User = {
  17. updateOne: sinon.stub().callsArgWith(3, null, { nModified: 1 }),
  18. }),
  19. },
  20. '../../infrastructure/mongodb': {
  21. db: (this.db = { users: {} }),
  22. ObjectId,
  23. },
  24. bcrypt: (this.bcrypt = {}),
  25. '@overleaf/settings': this.settings,
  26. '../User/UserGetter': (this.UserGetter = {}),
  27. './AuthenticationErrors': AuthenticationErrors,
  28. './HaveIBeenPwned': {
  29. checkPasswordForReuseInBackground: sinon.stub(),
  30. },
  31. '../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
  32. addEntry: sinon.stub().callsArgWith(5, null),
  33. }),
  34. },
  35. })
  36. this.callback = sinon.stub()
  37. })
  38. afterEach(function () {
  39. tk.reset()
  40. })
  41. describe('with real bcrypt', function () {
  42. beforeEach(function () {
  43. const bcrypt = require('bcrypt')
  44. this.bcrypt.compare = bcrypt.compare
  45. this.bcrypt.getRounds = bcrypt.getRounds
  46. this.bcrypt.genSalt = bcrypt.genSalt
  47. this.bcrypt.hash = bcrypt.hash
  48. // Hash of 'testpassword'
  49. this.testPassword =
  50. '$2a$04$DcU/3UeJf1PfsWlQL./5H.rGTQL1Z1iyz6r7bN9Do8cy6pVWxpKpK'
  51. })
  52. describe('authenticate', function () {
  53. beforeEach(function () {
  54. this.user = {
  55. _id: 'user-id',
  56. email: (this.email = 'USER@sharelatex.com'),
  57. }
  58. this.user.hashedPassword = this.testPassword
  59. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  60. })
  61. describe('when the hashed password matches', function () {
  62. beforeEach(function (done) {
  63. this.unencryptedPassword = 'testpassword'
  64. this.AuthenticationManager.authenticate(
  65. { email: this.email },
  66. this.unencryptedPassword,
  67. (error, user) => {
  68. this.callback(error, user)
  69. done()
  70. }
  71. )
  72. })
  73. it('should look up the correct user in the database', function () {
  74. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  75. })
  76. it('should bump epoch', function () {
  77. this.User.updateOne.should.have.been.calledWith(
  78. {
  79. _id: this.user._id,
  80. loginEpoch: this.user.loginEpoch,
  81. },
  82. {
  83. $inc: { loginEpoch: 1 },
  84. },
  85. {}
  86. )
  87. })
  88. it('should return the user', function () {
  89. this.callback.calledWith(null, this.user).should.equal(true)
  90. })
  91. })
  92. describe('when the encrypted passwords do not match', function () {
  93. beforeEach(function (done) {
  94. this.AuthenticationManager.authenticate(
  95. { email: this.email },
  96. 'notthecorrectpassword',
  97. (...args) => {
  98. this.callback(...args)
  99. done()
  100. }
  101. )
  102. })
  103. it('should persist the login failure and bump epoch', function () {
  104. this.User.updateOne.should.have.been.calledWith(
  105. {
  106. _id: this.user._id,
  107. loginEpoch: this.user.loginEpoch,
  108. },
  109. {
  110. $inc: { loginEpoch: 1 },
  111. $set: { lastFailedLogin: new Date() },
  112. }
  113. )
  114. })
  115. it('should not return the user', function () {
  116. this.callback.calledWith(null, null).should.equal(true)
  117. })
  118. })
  119. describe('when another request runs in parallel', function () {
  120. beforeEach(function () {
  121. this.User.updateOne = sinon
  122. .stub()
  123. .callsArgWith(3, null, { nModified: 0 })
  124. })
  125. describe('correct password', function () {
  126. beforeEach(function (done) {
  127. this.AuthenticationManager.authenticate(
  128. { email: this.email },
  129. 'testpassword',
  130. (...args) => {
  131. this.callback(...args)
  132. done()
  133. }
  134. )
  135. })
  136. it('should return an error', function () {
  137. this.callback.should.have.been.calledWith(
  138. sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
  139. )
  140. })
  141. })
  142. describe('bad password', function () {
  143. beforeEach(function (done) {
  144. this.User.updateOne = sinon.stub().yields(null, { nModified: 0 })
  145. this.AuthenticationManager.authenticate(
  146. { email: this.email },
  147. 'notthecorrectpassword',
  148. (...args) => {
  149. this.callback(...args)
  150. done()
  151. }
  152. )
  153. })
  154. it('should return an error', function () {
  155. this.callback.should.have.been.calledWith(
  156. sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
  157. )
  158. })
  159. })
  160. })
  161. })
  162. describe('setUserPasswordInV2', function () {
  163. beforeEach(function () {
  164. this.user = {
  165. _id: '5c8791477192a80b5e76ca7e',
  166. email: (this.email = 'USER@sharelatex.com'),
  167. }
  168. this.db.users.updateOne = sinon
  169. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  170. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  171. this.db.users.updateOne = sinon
  172. .stub()
  173. .callsArgWith(2, null, { modifiedCount: 1 })
  174. })
  175. it('should not produce an error', function (done) {
  176. this.AuthenticationManager.setUserPasswordInV2(
  177. this.user,
  178. 'testpassword',
  179. (err, updated) => {
  180. expect(err).to.not.exist
  181. expect(updated).to.equal(true)
  182. done()
  183. }
  184. )
  185. })
  186. it('should set the hashed password', function (done) {
  187. this.AuthenticationManager.setUserPasswordInV2(
  188. this.user,
  189. 'testpassword',
  190. err => {
  191. expect(err).to.not.exist
  192. const { hashedPassword } =
  193. this.db.users.updateOne.lastCall.args[1].$set
  194. expect(hashedPassword).to.exist
  195. expect(hashedPassword.length).to.equal(60)
  196. expect(hashedPassword).to.match(/^\$2a\$04\$[a-zA-Z0-9/.]{53}$/)
  197. done()
  198. }
  199. )
  200. })
  201. })
  202. })
  203. describe('hashPassword', function () {
  204. it('should block too long passwords', function (done) {
  205. this.AuthenticationManager.hashPassword('x'.repeat(100), err => {
  206. expect(err).to.exist
  207. expect(err.message).to.equal('password is too long')
  208. done()
  209. })
  210. })
  211. })
  212. describe('authenticate', function () {
  213. describe('when the user exists in the database', function () {
  214. beforeEach(function () {
  215. this.user = {
  216. _id: 'user-id',
  217. email: (this.email = 'USER@sharelatex.com'),
  218. }
  219. this.unencryptedPassword = 'banana'
  220. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  221. })
  222. describe('when the hashed password matches', function () {
  223. beforeEach(function (done) {
  224. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  225. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  226. this.bcrypt.getRounds = sinon.stub().returns(4)
  227. this.AuthenticationManager.authenticate(
  228. { email: this.email },
  229. this.unencryptedPassword,
  230. (error, user) => {
  231. this.callback(error, user)
  232. done()
  233. }
  234. )
  235. })
  236. it('should look up the correct user in the database', function () {
  237. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  238. })
  239. it('should check that the passwords match', function () {
  240. this.bcrypt.compare
  241. .calledWith(this.unencryptedPassword, this.hashedPassword)
  242. .should.equal(true)
  243. })
  244. it('should return the user', function () {
  245. this.callback.calledWith(null, this.user).should.equal(true)
  246. })
  247. })
  248. describe('when the encrypted passwords do not match', function () {
  249. beforeEach(function () {
  250. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  251. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  252. this.AuthenticationManager.authenticate(
  253. { email: this.email },
  254. this.unencryptedPassword,
  255. this.callback
  256. )
  257. })
  258. it('should not return the user', function () {
  259. this.callback.calledWith(null, null).should.equal(true)
  260. this.UserAuditLogHandler.addEntry.callCount.should.equal(0)
  261. })
  262. })
  263. describe('when the encrypted passwords do not match, with auditLog', function () {
  264. beforeEach(function () {
  265. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  266. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  267. this.auditLog = { ipAddress: 'ip', info: { method: 'foo' } }
  268. this.AuthenticationManager.authenticate(
  269. { email: this.email },
  270. this.unencryptedPassword,
  271. this.auditLog,
  272. this.callback
  273. )
  274. })
  275. it('should not return the user, but add entry to audit log', function () {
  276. this.callback.calledWith(null, null).should.equal(true)
  277. this.UserAuditLogHandler.addEntry.callCount.should.equal(1)
  278. this.UserAuditLogHandler.addEntry
  279. .calledWith(
  280. this.user._id,
  281. 'failed-password-match',
  282. this.user._id,
  283. this.auditLog.ipAddress,
  284. this.auditLog.info
  285. )
  286. .should.equal(true)
  287. })
  288. })
  289. describe('when the hashed password matches but the number of rounds is too low', function () {
  290. beforeEach(function (done) {
  291. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  292. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  293. this.bcrypt.getRounds = sinon.stub().returns(1)
  294. this.AuthenticationManager._setUserPasswordInMongo = sinon
  295. .stub()
  296. .callsArgWith(2, null)
  297. this.AuthenticationManager.authenticate(
  298. { email: this.email },
  299. this.unencryptedPassword,
  300. (error, user) => {
  301. this.callback(error, user)
  302. done()
  303. }
  304. )
  305. })
  306. it('should look up the correct user in the database', function () {
  307. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  308. })
  309. it('should check that the passwords match', function () {
  310. this.bcrypt.compare
  311. .calledWith(this.unencryptedPassword, this.hashedPassword)
  312. .should.equal(true)
  313. })
  314. it('should check the number of rounds', function () {
  315. this.bcrypt.getRounds.called.should.equal(true)
  316. })
  317. it('should set the users password (with a higher number of rounds)', function () {
  318. this.AuthenticationManager._setUserPasswordInMongo
  319. .calledWith(this.user, this.unencryptedPassword)
  320. .should.equal(true)
  321. })
  322. it('should return the user', function () {
  323. this.callback.calledWith(null, this.user).should.equal(true)
  324. })
  325. })
  326. describe('when the hashed password matches but the number of rounds is too low, but upgrades disabled', function () {
  327. beforeEach(function (done) {
  328. this.settings.security.disableBcryptRoundsUpgrades = true
  329. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  330. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  331. this.bcrypt.getRounds = sinon.stub().returns(1)
  332. this.AuthenticationManager.setUserPassword = sinon
  333. .stub()
  334. .callsArgWith(2, null)
  335. this.AuthenticationManager.authenticate(
  336. { email: this.email },
  337. this.unencryptedPassword,
  338. (error, user) => {
  339. this.callback(error, user)
  340. done()
  341. }
  342. )
  343. })
  344. it('should not check the number of rounds', function () {
  345. this.bcrypt.getRounds.called.should.equal(false)
  346. })
  347. it('should not set the users password (with a higher number of rounds)', function () {
  348. this.AuthenticationManager.setUserPassword
  349. .calledWith(this.user, this.unencryptedPassword)
  350. .should.equal(false)
  351. })
  352. it('should return the user', function () {
  353. this.callback.calledWith(null, this.user).should.equal(true)
  354. })
  355. })
  356. })
  357. describe('when the user does not exist in the database', function () {
  358. beforeEach(function () {
  359. this.User.findOne = sinon.stub().callsArgWith(1, null, null)
  360. this.AuthenticationManager.authenticate(
  361. { email: this.email },
  362. this.unencrpytedPassword,
  363. this.callback
  364. )
  365. })
  366. it('should not return a user', function () {
  367. this.callback.calledWith(null, null).should.equal(true)
  368. })
  369. })
  370. })
  371. describe('validateEmail', function () {
  372. describe('valid', function () {
  373. it('should return null', function () {
  374. const result =
  375. this.AuthenticationManager.validateEmail('foo@example.com')
  376. expect(result).to.equal(null)
  377. })
  378. })
  379. describe('invalid', function () {
  380. it('should return validation error object for no email', function () {
  381. const result = this.AuthenticationManager.validateEmail('')
  382. expect(result).to.an.instanceOf(AuthenticationErrors.InvalidEmailError)
  383. expect(result.message).to.equal('email not valid')
  384. })
  385. it('should return validation error object for invalid', function () {
  386. const result = this.AuthenticationManager.validateEmail('notanemail')
  387. expect(result).to.be.an.instanceOf(
  388. AuthenticationErrors.InvalidEmailError
  389. )
  390. expect(result.message).to.equal('email not valid')
  391. })
  392. })
  393. })
  394. describe('validatePassword', function () {
  395. beforeEach(function () {
  396. // 73 characters:
  397. this.longPassword =
  398. '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345678'
  399. })
  400. describe('with a null password', function () {
  401. it('should return an error', function () {
  402. const result = this.AuthenticationManager.validatePassword()
  403. expect(result).to.be.an.instanceOf(
  404. AuthenticationErrors.InvalidPasswordError
  405. )
  406. expect(result.message).to.equal('password not set')
  407. expect(result.info.code).to.equal('not_set')
  408. })
  409. })
  410. describe('password length', function () {
  411. describe('with the default password length options', function () {
  412. it('should reject passwords that are too short', function () {
  413. const result1 = this.AuthenticationManager.validatePassword('')
  414. expect(result1).to.be.an.instanceOf(
  415. AuthenticationErrors.InvalidPasswordError
  416. )
  417. expect(result1.message).to.equal('password is too short')
  418. expect(result1.info.code).to.equal('too_short')
  419. const result2 = this.AuthenticationManager.validatePassword('foo')
  420. expect(result2).to.be.an.instanceOf(
  421. AuthenticationErrors.InvalidPasswordError
  422. )
  423. expect(result2.message).to.equal('password is too short')
  424. expect(result2.info.code).to.equal('too_short')
  425. })
  426. it('should reject passwords that are too long', function () {
  427. const result = this.AuthenticationManager.validatePassword(
  428. this.longPassword
  429. )
  430. expect(result).to.be.an.instanceOf(
  431. AuthenticationErrors.InvalidPasswordError
  432. )
  433. expect(result.message).to.equal('password is too long')
  434. expect(result.info.code).to.equal('too_long')
  435. })
  436. it('should accept passwords that are a good length', function () {
  437. expect(
  438. this.AuthenticationManager.validatePassword('l337h4x0r')
  439. ).to.equal(null)
  440. })
  441. })
  442. describe('when the password length is specified in settings', function () {
  443. beforeEach(function () {
  444. this.settings.passwordStrengthOptions = {
  445. length: {
  446. min: 10,
  447. max: 12,
  448. },
  449. }
  450. })
  451. it('should reject passwords that are too short', function () {
  452. const result =
  453. this.AuthenticationManager.validatePassword('012345678')
  454. expect(result).to.be.an.instanceOf(
  455. AuthenticationErrors.InvalidPasswordError
  456. )
  457. expect(result.message).to.equal('password is too short')
  458. expect(result.info.code).to.equal('too_short')
  459. })
  460. it('should accept passwords of exactly minimum length', function () {
  461. expect(
  462. this.AuthenticationManager.validatePassword('0123456789')
  463. ).to.equal(null)
  464. })
  465. it('should reject passwords that are too long', function () {
  466. const result =
  467. this.AuthenticationManager.validatePassword('0123456789abc')
  468. expect(result).to.be.an.instanceOf(
  469. AuthenticationErrors.InvalidPasswordError
  470. )
  471. expect(result.message).to.equal('password is too long')
  472. expect(result.info.code).to.equal('too_long')
  473. })
  474. it('should accept passwords of exactly maximum length', function () {
  475. expect(
  476. this.AuthenticationManager.validatePassword('0123456789ab')
  477. ).to.equal(null)
  478. })
  479. })
  480. describe('when the maximum password length is set to >72 characters in settings', function () {
  481. beforeEach(function () {
  482. this.settings.passwordStrengthOptions = {
  483. length: {
  484. max: 128,
  485. },
  486. }
  487. })
  488. it('should still reject passwords > 72 characters in length', function () {
  489. const result = this.AuthenticationManager.validatePassword(
  490. this.longPassword
  491. )
  492. expect(result).to.be.an.instanceOf(
  493. AuthenticationErrors.InvalidPasswordError
  494. )
  495. expect(result.message).to.equal('password is too long')
  496. expect(result.info.code).to.equal('too_long')
  497. })
  498. })
  499. })
  500. describe('allowed characters', function () {
  501. describe('with the default settings for allowed characters', function () {
  502. it('should allow passwords with valid characters', function () {
  503. expect(
  504. this.AuthenticationManager.validatePassword(
  505. 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  506. )
  507. ).to.equal(null)
  508. expect(
  509. this.AuthenticationManager.validatePassword(
  510. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  511. )
  512. ).to.equal(null)
  513. })
  514. it('should not allow passwords with invalid characters', function () {
  515. const result = this.AuthenticationManager.validatePassword(
  516. 'correct horse battery staple'
  517. )
  518. expect(result).to.be.an.instanceOf(
  519. AuthenticationErrors.InvalidPasswordError
  520. )
  521. expect(result.message).to.equal(
  522. 'password contains an invalid character'
  523. )
  524. expect(result.info.code).to.equal('invalid_character')
  525. })
  526. })
  527. describe('when valid characters are overridden in settings', function () {
  528. beforeEach(function () {
  529. this.settings.passwordStrengthOptions = {
  530. chars: {
  531. symbols: ' ',
  532. },
  533. }
  534. })
  535. it('should allow passwords with valid characters', function () {
  536. expect(
  537. this.AuthenticationManager.validatePassword(
  538. 'correct horse battery staple'
  539. )
  540. ).to.equal(null)
  541. })
  542. it('should disallow passwords with invalid characters', function () {
  543. const result = this.AuthenticationManager.validatePassword(
  544. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  545. )
  546. expect(result).to.be.an.instanceOf(
  547. AuthenticationErrors.InvalidPasswordError
  548. )
  549. expect(result.message).to.equal(
  550. 'password contains an invalid character'
  551. )
  552. expect(result.info.code).to.equal('invalid_character')
  553. })
  554. })
  555. describe('when allowAnyChars is set', function () {
  556. beforeEach(function () {
  557. this.settings.passwordStrengthOptions = {
  558. allowAnyChars: true,
  559. }
  560. })
  561. it('should allow any characters', function () {
  562. expect(
  563. this.AuthenticationManager.validatePassword(
  564. 'correct horse battery staple'
  565. )
  566. ).to.equal(null)
  567. expect(
  568. this.AuthenticationManager.validatePassword(
  569. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  570. )
  571. ).to.equal(null)
  572. })
  573. })
  574. })
  575. })
  576. describe('setUserPassword', function () {
  577. beforeEach(function () {
  578. this.user_id = ObjectId()
  579. this.password = 'banana'
  580. this.hashedPassword = 'asdkjfa;osiuvandf'
  581. this.salt = 'saltaasdfasdfasdf'
  582. this.user = {
  583. _id: this.user_id,
  584. email: 'user@example.com',
  585. hashedPassword: this.hashedPassword,
  586. }
  587. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  588. this.bcrypt.genSalt = sinon.stub().callsArgWith(2, null, this.salt)
  589. this.bcrypt.hash = sinon.stub().callsArgWith(2, null, this.hashedPassword)
  590. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  591. this.db.users.updateOne = sinon.stub().callsArg(2)
  592. })
  593. describe('same as previous password', function () {
  594. beforeEach(function () {
  595. this.bcrypt.compare.callsArgWith(2, null, true)
  596. })
  597. it('should return an error', function (done) {
  598. this.AuthenticationManager.setUserPassword(
  599. this.user,
  600. this.password,
  601. err => {
  602. expect(err).to.exist
  603. expect(err.name).to.equal('PasswordMustBeDifferentError')
  604. done()
  605. }
  606. )
  607. })
  608. })
  609. describe('too long', function () {
  610. beforeEach(function () {
  611. this.settings.passwordStrengthOptions = {
  612. length: {
  613. max: 10,
  614. },
  615. }
  616. this.password = 'dsdsadsadsadsadsadkjsadjsadjsadljs'
  617. })
  618. it('should return and error', function (done) {
  619. this.AuthenticationManager.setUserPassword(
  620. this.user,
  621. this.password,
  622. err => {
  623. expect(err).to.exist
  624. done()
  625. }
  626. )
  627. })
  628. it('should not start the bcrypt process', function (done) {
  629. this.AuthenticationManager.setUserPassword(
  630. this.user,
  631. this.password,
  632. () => {
  633. this.bcrypt.genSalt.called.should.equal(false)
  634. this.bcrypt.hash.called.should.equal(false)
  635. done()
  636. }
  637. )
  638. })
  639. })
  640. describe('contains full email', function () {
  641. beforeEach(function () {
  642. this.password = `some${this.user.email}password`
  643. })
  644. it('should reject the password', function (done) {
  645. this.AuthenticationManager.setUserPassword(
  646. this.user,
  647. this.password,
  648. err => {
  649. expect(err).to.exist
  650. expect(err.name).to.equal('InvalidPasswordError')
  651. done()
  652. }
  653. )
  654. })
  655. })
  656. describe('contains first part of email', function () {
  657. beforeEach(function () {
  658. this.password = `some${this.user.email.split('@')[0]}password`
  659. })
  660. it('should reject the password', function (done) {
  661. this.AuthenticationManager.setUserPassword(
  662. this.user,
  663. this.password,
  664. err => {
  665. expect(err).to.exist
  666. expect(err.name).to.equal('InvalidPasswordError')
  667. done()
  668. }
  669. )
  670. })
  671. })
  672. describe('too short', function () {
  673. beforeEach(function () {
  674. this.settings.passwordStrengthOptions = {
  675. length: {
  676. max: 10,
  677. min: 6,
  678. },
  679. }
  680. this.password = 'dsd'
  681. })
  682. it('should return and error', function (done) {
  683. this.AuthenticationManager.setUserPassword(
  684. this.user,
  685. this.password,
  686. err => {
  687. expect(err).to.exist
  688. done()
  689. }
  690. )
  691. })
  692. it('should not start the bcrypt process', function (done) {
  693. this.AuthenticationManager.setUserPassword(
  694. this.user,
  695. this.password,
  696. () => {
  697. this.bcrypt.genSalt.called.should.equal(false)
  698. this.bcrypt.hash.called.should.equal(false)
  699. done()
  700. }
  701. )
  702. })
  703. })
  704. describe('successful password set attempt', function () {
  705. beforeEach(function () {
  706. this.UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
  707. this.AuthenticationManager.setUserPassword(
  708. this.user,
  709. this.password,
  710. this.callback
  711. )
  712. })
  713. it("should update the user's password in the database", function () {
  714. const { args } = this.db.users.updateOne.lastCall
  715. expect(args[0]).to.deep.equal({
  716. _id: ObjectId(this.user_id.toString()),
  717. })
  718. expect(args[1]).to.deep.equal({
  719. $set: {
  720. hashedPassword: this.hashedPassword,
  721. },
  722. $unset: {
  723. password: true,
  724. },
  725. })
  726. })
  727. it('should hash the password', function () {
  728. this.bcrypt.genSalt.calledWith(4).should.equal(true)
  729. this.bcrypt.hash.calledWith(this.password, this.salt).should.equal(true)
  730. })
  731. it('should call the callback', function () {
  732. this.callback.called.should.equal(true)
  733. })
  734. })
  735. })
  736. })