AuthenticationManagerTests.js 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  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.metrics = { inc: sinon.stub().returns() }
  14. this.AuthenticationManager = SandboxedModule.require(modulePath, {
  15. requires: {
  16. '../../models/User': {
  17. User: (this.User = {
  18. updateOne: sinon.stub().callsArgWith(3, null, { modifiedCount: 1 }),
  19. }),
  20. },
  21. '../../infrastructure/mongodb': {
  22. db: (this.db = { users: {} }),
  23. ObjectId,
  24. },
  25. bcrypt: (this.bcrypt = {}),
  26. '@overleaf/settings': this.settings,
  27. '../User/UserGetter': (this.UserGetter = {}),
  28. './AuthenticationErrors': AuthenticationErrors,
  29. './HaveIBeenPwned': {
  30. checkPasswordForReuse: sinon.stub().yields(null, false),
  31. checkPasswordForReuseInBackground: sinon.stub(),
  32. },
  33. '../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
  34. addEntry: sinon.stub().callsArgWith(5, null),
  35. }),
  36. '@overleaf/metrics': this.metrics,
  37. },
  38. })
  39. this.callback = sinon.stub()
  40. })
  41. afterEach(function () {
  42. tk.reset()
  43. })
  44. describe('with real bcrypt', function () {
  45. beforeEach(function () {
  46. const bcrypt = require('bcrypt')
  47. this.bcrypt.compare = bcrypt.compare
  48. this.bcrypt.getRounds = bcrypt.getRounds
  49. this.bcrypt.genSalt = bcrypt.genSalt
  50. this.bcrypt.hash = bcrypt.hash
  51. // Hash of 'testpassword'
  52. this.testPassword =
  53. '$2a$04$DcU/3UeJf1PfsWlQL./5H.rGTQL1Z1iyz6r7bN9Do8cy6pVWxpKpK'
  54. })
  55. describe('authenticate', function () {
  56. beforeEach(function () {
  57. this.user = {
  58. _id: 'user-id',
  59. email: (this.email = 'USER@sharelatex.com'),
  60. }
  61. this.user.hashedPassword = this.testPassword
  62. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  63. this.metrics.inc.reset()
  64. })
  65. describe('when the hashed password matches', function () {
  66. beforeEach(function (done) {
  67. this.unencryptedPassword = 'testpassword'
  68. this.AuthenticationManager.authenticate(
  69. { email: this.email },
  70. this.unencryptedPassword,
  71. (error, user) => {
  72. this.callback(error, user)
  73. done()
  74. }
  75. )
  76. })
  77. it('should look up the correct user in the database', function () {
  78. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  79. })
  80. it('should bump epoch', function () {
  81. this.User.updateOne.should.have.been.calledWith(
  82. {
  83. _id: this.user._id,
  84. loginEpoch: this.user.loginEpoch,
  85. },
  86. {
  87. $inc: { loginEpoch: 1 },
  88. },
  89. {}
  90. )
  91. })
  92. it('should return the user', function () {
  93. this.callback.should.have.been.calledWith(null, this.user)
  94. })
  95. it('should send metrics', function () {
  96. expect(
  97. this.metrics.inc.calledWith('check-password', { status: 'success' })
  98. ).to.equal(true)
  99. })
  100. })
  101. describe('when the encrypted passwords do not match', function () {
  102. beforeEach(function (done) {
  103. this.AuthenticationManager.authenticate(
  104. { email: this.email },
  105. 'notthecorrectpassword',
  106. (...args) => {
  107. this.callback(...args)
  108. done()
  109. }
  110. )
  111. })
  112. it('should persist the login failure and bump epoch', function () {
  113. this.User.updateOne.should.have.been.calledWith(
  114. {
  115. _id: this.user._id,
  116. loginEpoch: this.user.loginEpoch,
  117. },
  118. {
  119. $inc: { loginEpoch: 1 },
  120. $set: { lastFailedLogin: new Date() },
  121. }
  122. )
  123. })
  124. it('should not return the user', function () {
  125. this.callback.calledWith(null, null).should.equal(true)
  126. })
  127. it('should not send metrics', function () {
  128. expect(this.metrics.inc.called).to.equal(false)
  129. })
  130. })
  131. describe('when another request runs in parallel', function () {
  132. beforeEach(function () {
  133. this.User.updateOne = sinon
  134. .stub()
  135. .callsArgWith(3, null, { modifiedCount: 0 })
  136. })
  137. describe('correct password', function () {
  138. beforeEach(function (done) {
  139. this.AuthenticationManager.authenticate(
  140. { email: this.email },
  141. 'testpassword',
  142. (...args) => {
  143. this.callback(...args)
  144. done()
  145. }
  146. )
  147. })
  148. it('should return an error', function () {
  149. this.callback.should.have.been.calledWith(
  150. sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
  151. )
  152. })
  153. })
  154. describe('bad password', function () {
  155. beforeEach(function (done) {
  156. this.User.updateOne = sinon
  157. .stub()
  158. .yields(null, { modifiedCount: 0 })
  159. this.AuthenticationManager.authenticate(
  160. { email: this.email },
  161. 'notthecorrectpassword',
  162. (...args) => {
  163. this.callback(...args)
  164. done()
  165. }
  166. )
  167. })
  168. it('should return an error', function () {
  169. this.callback.should.have.been.calledWith(
  170. sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
  171. )
  172. })
  173. })
  174. })
  175. })
  176. describe('setUserPasswordInV2', function () {
  177. beforeEach(function () {
  178. this.user = {
  179. _id: '5c8791477192a80b5e76ca7e',
  180. email: (this.email = 'USER@sharelatex.com'),
  181. }
  182. this.db.users.updateOne = sinon
  183. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  184. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  185. this.db.users.updateOne = sinon
  186. .stub()
  187. .callsArgWith(2, null, { modifiedCount: 1 })
  188. })
  189. it('should not produce an error', function (done) {
  190. this.AuthenticationManager.setUserPasswordInV2(
  191. this.user,
  192. 'testpassword',
  193. (err, updated) => {
  194. expect(err).to.not.exist
  195. expect(updated).to.equal(true)
  196. done()
  197. }
  198. )
  199. })
  200. it('should set the hashed password', function (done) {
  201. this.AuthenticationManager.setUserPasswordInV2(
  202. this.user,
  203. 'testpassword',
  204. err => {
  205. expect(err).to.not.exist
  206. const { hashedPassword } =
  207. this.db.users.updateOne.lastCall.args[1].$set
  208. expect(hashedPassword).to.exist
  209. expect(hashedPassword.length).to.equal(60)
  210. expect(hashedPassword).to.match(/^\$2a\$04\$[a-zA-Z0-9/.]{53}$/)
  211. done()
  212. }
  213. )
  214. })
  215. })
  216. })
  217. describe('hashPassword', function () {
  218. it('should block too long passwords', function (done) {
  219. this.AuthenticationManager.hashPassword('x'.repeat(100), err => {
  220. expect(err).to.exist
  221. expect(err.message).to.equal('password is too long')
  222. done()
  223. })
  224. })
  225. })
  226. describe('authenticate', function () {
  227. describe('when the user exists in the database', function () {
  228. beforeEach(function () {
  229. this.user = {
  230. _id: 'user-id',
  231. email: (this.email = 'USER@sharelatex.com'),
  232. }
  233. this.unencryptedPassword = 'banana'
  234. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  235. this.metrics.inc.reset()
  236. })
  237. describe('when the hashed password matches', function () {
  238. beforeEach(function (done) {
  239. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  240. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  241. this.bcrypt.getRounds = sinon.stub().returns(4)
  242. this.AuthenticationManager.authenticate(
  243. { email: this.email },
  244. this.unencryptedPassword,
  245. (error, user) => {
  246. this.callback(error, user)
  247. done()
  248. }
  249. )
  250. })
  251. it('should look up the correct user in the database', function () {
  252. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  253. })
  254. it('should check that the passwords match', function () {
  255. this.bcrypt.compare
  256. .calledWith(this.unencryptedPassword, this.hashedPassword)
  257. .should.equal(true)
  258. })
  259. it('should send metrics', function () {
  260. expect(
  261. this.metrics.inc.calledWith('check-password', {
  262. status: 'too_short',
  263. })
  264. ).to.equal(true)
  265. })
  266. it('should return the user', function () {
  267. this.callback.calledWith(null, this.user).should.equal(true)
  268. })
  269. })
  270. describe('when the encrypted passwords do not match', function () {
  271. beforeEach(function () {
  272. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  273. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  274. this.AuthenticationManager.authenticate(
  275. { email: this.email },
  276. this.unencryptedPassword,
  277. this.callback
  278. )
  279. })
  280. it('should not send metrics', function () {
  281. expect(this.metrics.inc.called).to.equal(false)
  282. })
  283. it('should not return the user', function () {
  284. this.callback.calledWith(null, null).should.equal(true)
  285. this.UserAuditLogHandler.addEntry.callCount.should.equal(0)
  286. })
  287. })
  288. describe('when the encrypted passwords do not match, with auditLog', function () {
  289. beforeEach(function () {
  290. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  291. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  292. this.auditLog = { ipAddress: 'ip', info: { method: 'foo' } }
  293. this.AuthenticationManager.authenticate(
  294. { email: this.email },
  295. this.unencryptedPassword,
  296. this.auditLog,
  297. this.callback
  298. )
  299. })
  300. it('should not return the user, but add entry to audit log', function () {
  301. this.callback.calledWith(null, null).should.equal(true)
  302. this.UserAuditLogHandler.addEntry.callCount.should.equal(1)
  303. this.UserAuditLogHandler.addEntry
  304. .calledWith(
  305. this.user._id,
  306. 'failed-password-match',
  307. this.user._id,
  308. this.auditLog.ipAddress,
  309. this.auditLog.info
  310. )
  311. .should.equal(true)
  312. })
  313. })
  314. describe('when the hashed password matches but the number of rounds is too low', function () {
  315. beforeEach(function (done) {
  316. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  317. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  318. this.bcrypt.getRounds = sinon.stub().returns(1)
  319. this.AuthenticationManager._setUserPasswordInMongo = sinon
  320. .stub()
  321. .callsArgWith(2, null)
  322. this.AuthenticationManager.authenticate(
  323. { email: this.email },
  324. this.unencryptedPassword,
  325. (error, user) => {
  326. this.callback(error, user)
  327. done()
  328. }
  329. )
  330. })
  331. it('should look up the correct user in the database', function () {
  332. this.User.findOne.calledWith({ email: this.email }).should.equal(true)
  333. })
  334. it('should check that the passwords match', function () {
  335. this.bcrypt.compare
  336. .calledWith(this.unencryptedPassword, this.hashedPassword)
  337. .should.equal(true)
  338. })
  339. it('should check the number of rounds', function () {
  340. this.bcrypt.getRounds.called.should.equal(true)
  341. })
  342. it('should set the users password (with a higher number of rounds)', function () {
  343. this.AuthenticationManager._setUserPasswordInMongo
  344. .calledWith(this.user, this.unencryptedPassword)
  345. .should.equal(true)
  346. })
  347. it('should return the user', function () {
  348. this.callback.calledWith(null, this.user).should.equal(true)
  349. })
  350. })
  351. describe('when the hashed password matches but the number of rounds is too low, but upgrades disabled', function () {
  352. beforeEach(function (done) {
  353. this.settings.security.disableBcryptRoundsUpgrades = true
  354. this.user.hashedPassword = this.hashedPassword = 'asdfjadflasdf'
  355. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  356. this.bcrypt.getRounds = sinon.stub().returns(1)
  357. this.AuthenticationManager.setUserPassword = sinon
  358. .stub()
  359. .callsArgWith(2, null)
  360. this.AuthenticationManager.authenticate(
  361. { email: this.email },
  362. this.unencryptedPassword,
  363. (error, user) => {
  364. this.callback(error, user)
  365. done()
  366. }
  367. )
  368. })
  369. it('should not check the number of rounds', function () {
  370. this.bcrypt.getRounds.called.should.equal(false)
  371. })
  372. it('should not set the users password (with a higher number of rounds)', function () {
  373. this.AuthenticationManager.setUserPassword
  374. .calledWith(this.user, this.unencryptedPassword)
  375. .should.equal(false)
  376. })
  377. it('should return the user', function () {
  378. this.callback.calledWith(null, this.user).should.equal(true)
  379. })
  380. })
  381. })
  382. describe('when the user does not exist in the database', function () {
  383. beforeEach(function () {
  384. this.User.findOne = sinon.stub().callsArgWith(1, null, null)
  385. this.AuthenticationManager.authenticate(
  386. { email: this.email },
  387. this.unencrpytedPassword,
  388. this.callback
  389. )
  390. })
  391. it('should not return a user', function () {
  392. this.callback.calledWith(null, null).should.equal(true)
  393. })
  394. })
  395. })
  396. describe('validateEmail', function () {
  397. describe('valid', function () {
  398. it('should return null', function () {
  399. const result =
  400. this.AuthenticationManager.validateEmail('foo@example.com')
  401. expect(result).to.equal(null)
  402. })
  403. })
  404. describe('invalid', function () {
  405. it('should return validation error object for no email', function () {
  406. const result = this.AuthenticationManager.validateEmail('')
  407. expect(result).to.an.instanceOf(AuthenticationErrors.InvalidEmailError)
  408. expect(result.message).to.equal('email not valid')
  409. })
  410. it('should return validation error object for invalid', function () {
  411. const result = this.AuthenticationManager.validateEmail('notanemail')
  412. expect(result).to.be.an.instanceOf(
  413. AuthenticationErrors.InvalidEmailError
  414. )
  415. expect(result.message).to.equal('email not valid')
  416. })
  417. })
  418. })
  419. describe('validatePassword', function () {
  420. beforeEach(function () {
  421. // 73 characters:
  422. this.longPassword =
  423. '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345678'
  424. })
  425. describe('with a null password', function () {
  426. it('should return an error', function () {
  427. const result = this.AuthenticationManager.validatePassword()
  428. expect(result).to.be.an.instanceOf(
  429. AuthenticationErrors.InvalidPasswordError
  430. )
  431. expect(result.message).to.equal('password not set')
  432. expect(result.info.code).to.equal('not_set')
  433. })
  434. })
  435. describe('password length', function () {
  436. describe('with the default password length options', function () {
  437. beforeEach(function () {
  438. this.metrics.inc.reset()
  439. })
  440. it('should send a metric', function () {
  441. this.AuthenticationManager.validatePassword('foo')
  442. expect(this.metrics.inc.calledWith('try-validate-password')).to.equal(
  443. true
  444. )
  445. })
  446. it('should reject passwords that are too short', function () {
  447. const result1 = this.AuthenticationManager.validatePassword('')
  448. expect(result1).to.be.an.instanceOf(
  449. AuthenticationErrors.InvalidPasswordError
  450. )
  451. expect(result1.message).to.equal('password is too short')
  452. expect(result1.info.code).to.equal('too_short')
  453. const result2 = this.AuthenticationManager.validatePassword('foo')
  454. expect(result2).to.be.an.instanceOf(
  455. AuthenticationErrors.InvalidPasswordError
  456. )
  457. expect(result2.message).to.equal('password is too short')
  458. expect(result2.info.code).to.equal('too_short')
  459. })
  460. it('should reject passwords that are too long', function () {
  461. const result = this.AuthenticationManager.validatePassword(
  462. this.longPassword
  463. )
  464. expect(result).to.be.an.instanceOf(
  465. AuthenticationErrors.InvalidPasswordError
  466. )
  467. expect(result.message).to.equal('password is too long')
  468. expect(result.info.code).to.equal('too_long')
  469. })
  470. it('should accept passwords that are a good length', function () {
  471. expect(
  472. this.AuthenticationManager.validatePassword('l337h4x0r')
  473. ).to.equal(null)
  474. })
  475. })
  476. describe('when the password length is specified in settings', function () {
  477. beforeEach(function () {
  478. this.settings.passwordStrengthOptions = {
  479. length: {
  480. min: 10,
  481. max: 12,
  482. },
  483. }
  484. })
  485. it('should reject passwords that are too short', function () {
  486. const result =
  487. this.AuthenticationManager.validatePassword('012345678')
  488. expect(result).to.be.an.instanceOf(
  489. AuthenticationErrors.InvalidPasswordError
  490. )
  491. expect(result.message).to.equal('password is too short')
  492. expect(result.info.code).to.equal('too_short')
  493. })
  494. it('should accept passwords of exactly minimum length', function () {
  495. expect(
  496. this.AuthenticationManager.validatePassword('0123456789')
  497. ).to.equal(null)
  498. })
  499. it('should reject passwords that are too long', function () {
  500. const result =
  501. this.AuthenticationManager.validatePassword('0123456789abc')
  502. expect(result).to.be.an.instanceOf(
  503. AuthenticationErrors.InvalidPasswordError
  504. )
  505. expect(result.message).to.equal('password is too long')
  506. expect(result.info.code).to.equal('too_long')
  507. })
  508. it('should accept passwords of exactly maximum length', function () {
  509. expect(
  510. this.AuthenticationManager.validatePassword('0123456789ab')
  511. ).to.equal(null)
  512. })
  513. })
  514. describe('when the maximum password length is set to >72 characters in settings', function () {
  515. beforeEach(function () {
  516. this.settings.passwordStrengthOptions = {
  517. length: {
  518. max: 128,
  519. },
  520. }
  521. })
  522. it('should still reject passwords > 72 characters in length', function () {
  523. const result = this.AuthenticationManager.validatePassword(
  524. this.longPassword
  525. )
  526. expect(result).to.be.an.instanceOf(
  527. AuthenticationErrors.InvalidPasswordError
  528. )
  529. expect(result.message).to.equal('password is too long')
  530. expect(result.info.code).to.equal('too_long')
  531. })
  532. })
  533. })
  534. describe('allowed characters', function () {
  535. describe('with the default settings for allowed characters', function () {
  536. it('should allow passwords with valid characters', function () {
  537. expect(
  538. this.AuthenticationManager.validatePassword(
  539. 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  540. )
  541. ).to.equal(null)
  542. expect(
  543. this.AuthenticationManager.validatePassword(
  544. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  545. )
  546. ).to.equal(null)
  547. })
  548. it('should not allow passwords with invalid characters', function () {
  549. const result = this.AuthenticationManager.validatePassword(
  550. 'correct horse battery staple'
  551. )
  552. expect(result).to.be.an.instanceOf(
  553. AuthenticationErrors.InvalidPasswordError
  554. )
  555. expect(result.message).to.equal(
  556. 'password contains an invalid character'
  557. )
  558. expect(result.info.code).to.equal('invalid_character')
  559. })
  560. })
  561. describe('when valid characters are overridden in settings', function () {
  562. beforeEach(function () {
  563. this.settings.passwordStrengthOptions = {
  564. chars: {
  565. symbols: ' ',
  566. },
  567. }
  568. })
  569. it('should allow passwords with valid characters', function () {
  570. expect(
  571. this.AuthenticationManager.validatePassword(
  572. 'correct horse battery staple'
  573. )
  574. ).to.equal(null)
  575. })
  576. it('should disallow passwords with invalid characters', function () {
  577. const result = this.AuthenticationManager.validatePassword(
  578. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  579. )
  580. expect(result).to.be.an.instanceOf(
  581. AuthenticationErrors.InvalidPasswordError
  582. )
  583. expect(result.message).to.equal(
  584. 'password contains an invalid character'
  585. )
  586. expect(result.info.code).to.equal('invalid_character')
  587. })
  588. })
  589. describe('when allowAnyChars is set', function () {
  590. beforeEach(function () {
  591. this.settings.passwordStrengthOptions = {
  592. allowAnyChars: true,
  593. }
  594. })
  595. it('should allow any characters', function () {
  596. expect(
  597. this.AuthenticationManager.validatePassword(
  598. 'correct horse battery staple'
  599. )
  600. ).to.equal(null)
  601. expect(
  602. this.AuthenticationManager.validatePassword(
  603. '1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,'
  604. )
  605. ).to.equal(null)
  606. })
  607. })
  608. })
  609. })
  610. describe('_validatePasswordNotContainsEmailSubstrings', function () {
  611. it('should return nothing for a dissimilar password', function () {
  612. const password = 'fublmqgaeohhvd8'
  613. const email = 'someuser@example.com'
  614. const error =
  615. this.AuthenticationManager._validatePasswordNotContainsEmailSubstrings(
  616. password,
  617. email
  618. )
  619. expect(error).to.not.exist
  620. })
  621. it('should return an error for password that is same as email', function () {
  622. const email = 'someuser@example.com'
  623. const error =
  624. this.AuthenticationManager._validatePasswordNotContainsEmailSubstrings(
  625. email,
  626. email
  627. )
  628. expect(error).to.exist
  629. })
  630. it('should return an error for a password with a substring of email', function () {
  631. const password = 'cooluser1253'
  632. const email = 'somecooluser@example.com'
  633. const error =
  634. this.AuthenticationManager._validatePasswordNotContainsEmailSubstrings(
  635. password,
  636. email
  637. )
  638. expect(error).to.exist
  639. })
  640. it('should return an error for a password with a substring of email, regardless of case', function () {
  641. const password = 'coOLUSer1253'
  642. const email = 'somecooluser@example.com'
  643. const error =
  644. this.AuthenticationManager._validatePasswordNotContainsEmailSubstrings(
  645. password,
  646. email
  647. )
  648. expect(error).to.exist
  649. })
  650. it('should return nothing for a password containing first two characters of email', function () {
  651. const password = 'lmgaesopxzqg'
  652. const email = 'someuser@example.com'
  653. const error =
  654. this.AuthenticationManager._validatePasswordNotContainsEmailSubstrings(
  655. password,
  656. email
  657. )
  658. expect(error).to.not.exist
  659. })
  660. })
  661. describe('_validatePasswordNotTooSimilar', function () {
  662. beforeEach(function () {
  663. this.metrics.inc.reset()
  664. })
  665. it('should return an error when the password is too similar to email', function () {
  666. const password = 'someuser1234'
  667. const email = 'someuser@example.com'
  668. const error = this.AuthenticationManager._validatePasswordNotTooSimilar(
  669. password,
  670. email
  671. )
  672. expect(error).to.exist
  673. })
  674. it('should return an error when the password is re-arranged elements of the email', function () {
  675. const password = 'su2oe1em3re'
  676. const email = 'someuser@example.com'
  677. const error = this.AuthenticationManager._validatePasswordNotTooSimilar(
  678. password,
  679. email
  680. )
  681. expect(error).to.exist
  682. })
  683. it('should send a metric with a rounded similarity score when password is too similar to email', function () {
  684. const password = 'su2oe1em3re'
  685. const email = 'someuser@example.com'
  686. const error = this.AuthenticationManager._validatePasswordNotTooSimilar(
  687. password,
  688. email
  689. )
  690. expect(
  691. this.metrics.inc.calledWith('password-validation-similarity', 1, {
  692. similarity: 0.7,
  693. })
  694. ).to.equal(true)
  695. expect(error).to.exist
  696. })
  697. it('should return nothing when the password different from email', function () {
  698. const password = '58WyLvr'
  699. const email = 'someuser@example.com'
  700. const error = this.AuthenticationManager._validatePasswordNotTooSimilar(
  701. password,
  702. email
  703. )
  704. expect(error).to.not.exist
  705. })
  706. it('should return nothing when the password is much longer than parts of the email', function () {
  707. const password = new Array(30).fill('a').join('')
  708. const email = 'a@cd.com'
  709. const error = this.AuthenticationManager._validatePasswordNotTooSimilar(
  710. password,
  711. email
  712. )
  713. expect(error).to.not.exist
  714. })
  715. })
  716. describe('setUserPassword', function () {
  717. beforeEach(function () {
  718. this.user_id = ObjectId()
  719. this.password = 'bananagram'
  720. this.hashedPassword = 'asdkjfa;osiuvandf'
  721. this.salt = 'saltaasdfasdfasdf'
  722. this.user = {
  723. _id: this.user_id,
  724. email: 'user@example.com',
  725. hashedPassword: this.hashedPassword,
  726. }
  727. this.bcrypt.compare = sinon.stub().callsArgWith(2, null, false)
  728. this.bcrypt.genSalt = sinon.stub().callsArgWith(2, null, this.salt)
  729. this.bcrypt.hash = sinon.stub().callsArgWith(2, null, this.hashedPassword)
  730. this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
  731. this.db.users.updateOne = sinon.stub().callsArg(2)
  732. })
  733. describe('same as previous password', function () {
  734. beforeEach(function () {
  735. this.bcrypt.compare.callsArgWith(2, null, true)
  736. })
  737. it('should return an error', function (done) {
  738. this.AuthenticationManager.setUserPassword(
  739. this.user,
  740. this.password,
  741. err => {
  742. expect(err).to.exist
  743. expect(err.name).to.equal('PasswordMustBeDifferentError')
  744. done()
  745. }
  746. )
  747. })
  748. })
  749. describe('too long', function () {
  750. beforeEach(function () {
  751. this.settings.passwordStrengthOptions = {
  752. length: {
  753. max: 10,
  754. },
  755. }
  756. this.password = 'dsdsadsadsadsadsadkjsadjsadjsadljs'
  757. })
  758. it('should return and error', function (done) {
  759. this.AuthenticationManager.setUserPassword(
  760. this.user,
  761. this.password,
  762. err => {
  763. expect(err).to.exist
  764. done()
  765. }
  766. )
  767. })
  768. it('should not start the bcrypt process', function (done) {
  769. this.AuthenticationManager.setUserPassword(
  770. this.user,
  771. this.password,
  772. () => {
  773. this.bcrypt.genSalt.called.should.equal(false)
  774. this.bcrypt.hash.called.should.equal(false)
  775. done()
  776. }
  777. )
  778. })
  779. })
  780. describe('contains full email', function () {
  781. beforeEach(function () {
  782. this.password = `some${this.user.email}password`
  783. })
  784. it('should reject the password', function (done) {
  785. this.AuthenticationManager.setUserPassword(
  786. this.user,
  787. this.password,
  788. err => {
  789. expect(err).to.exist
  790. expect(err.name).to.equal('InvalidPasswordError')
  791. done()
  792. }
  793. )
  794. })
  795. })
  796. describe('contains first part of email', function () {
  797. beforeEach(function () {
  798. this.password = `some${this.user.email.split('@')[0]}password`
  799. })
  800. it('should reject the password', function (done) {
  801. this.AuthenticationManager.setUserPassword(
  802. this.user,
  803. this.password,
  804. err => {
  805. expect(err).to.exist
  806. expect(err.name).to.equal('InvalidPasswordError')
  807. done()
  808. }
  809. )
  810. })
  811. })
  812. describe('too short', function () {
  813. beforeEach(function () {
  814. this.settings.passwordStrengthOptions = {
  815. length: {
  816. max: 10,
  817. min: 6,
  818. },
  819. }
  820. this.password = 'dsd'
  821. })
  822. it('should return and error', function (done) {
  823. this.AuthenticationManager.setUserPassword(
  824. this.user,
  825. this.password,
  826. err => {
  827. expect(err).to.exist
  828. done()
  829. }
  830. )
  831. })
  832. it('should not start the bcrypt process', function (done) {
  833. this.AuthenticationManager.setUserPassword(
  834. this.user,
  835. this.password,
  836. () => {
  837. this.bcrypt.genSalt.called.should.equal(false)
  838. this.bcrypt.hash.called.should.equal(false)
  839. done()
  840. }
  841. )
  842. })
  843. })
  844. describe('password too similar to email', function () {
  845. beforeEach(function () {
  846. this.user.email = 'foobarbazquux@example.com'
  847. this.password = 'foobarbaz'
  848. this.metrics.inc.reset()
  849. })
  850. it('should send a metric when the password is too similar to the email', function (done) {
  851. this.AuthenticationManager.setUserPassword(
  852. this.user,
  853. this.password,
  854. err => {
  855. expect(err).to.not.exist
  856. expect(
  857. this.metrics.inc.calledWith('password-too-similar-to-email')
  858. ).to.equal(true)
  859. done()
  860. }
  861. )
  862. })
  863. it('should send a metric when the password is too similar to the email, regardless of case', function (done) {
  864. this.AuthenticationManager.setUserPassword(
  865. this.user,
  866. this.password.toUpperCase(),
  867. err => {
  868. expect(err).to.not.exist
  869. expect(
  870. this.metrics.inc.calledWith('password-too-similar-to-email')
  871. ).to.equal(true)
  872. done()
  873. }
  874. )
  875. })
  876. })
  877. describe('password contains substring of email', function () {
  878. beforeEach(function () {
  879. this.user.email = 'somecooluser@example.com'
  880. this.password = 'somecoolfhzxk'
  881. this.metrics.inc.reset()
  882. })
  883. it('should send a metric when the password contains substring of the email', function (done) {
  884. this.AuthenticationManager.setUserPassword(
  885. this.user,
  886. this.password,
  887. err => {
  888. expect(err).to.not.exist
  889. expect(
  890. this.metrics.inc.calledWith(
  891. 'password-contains-substring-of-email'
  892. )
  893. ).to.equal(true)
  894. done()
  895. }
  896. )
  897. })
  898. })
  899. describe('successful password set attempt', function () {
  900. beforeEach(function () {
  901. this.metrics.inc.reset()
  902. this.UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
  903. this.AuthenticationManager.setUserPassword(
  904. this.user,
  905. this.password,
  906. this.callback
  907. )
  908. })
  909. it("should update the user's password in the database", function () {
  910. const { args } = this.db.users.updateOne.lastCall
  911. expect(args[0]).to.deep.equal({
  912. _id: ObjectId(this.user_id.toString()),
  913. })
  914. expect(args[1]).to.deep.equal({
  915. $set: {
  916. hashedPassword: this.hashedPassword,
  917. },
  918. $unset: {
  919. password: true,
  920. },
  921. })
  922. })
  923. it('should hash the password', function () {
  924. this.bcrypt.genSalt.calledWith(4).should.equal(true)
  925. this.bcrypt.hash.calledWith(this.password, this.salt).should.equal(true)
  926. })
  927. it('should not send a metric for password-too-similar-to-email', function () {
  928. expect(
  929. this.metrics.inc.calledWith('password-too-similar-to-email')
  930. ).to.equal(false)
  931. })
  932. it('should not send a metric for password-contains-substring-of-email', function () {
  933. expect(
  934. this.metrics.inc.calledWith('password-contains-substring-of-email')
  935. ).to.equal(false)
  936. })
  937. it('should call the callback', function () {
  938. this.callback.called.should.equal(true)
  939. })
  940. })
  941. })
  942. })