AuthenticationManagerTests.coffee 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. sinon = require('sinon')
  2. chai = require('chai')
  3. should = chai.should()
  4. expect = chai.expect
  5. modulePath = "../../../../app/js/Features/Authentication/AuthenticationManager.js"
  6. SandboxedModule = require('sandboxed-module')
  7. events = require "events"
  8. ObjectId = require("mongojs").ObjectId
  9. Errors = require "../../../../app/js/Features/Errors/Errors"
  10. describe "AuthenticationManager", ->
  11. beforeEach ->
  12. @settings = { security: { bcryptRounds: 12 } }
  13. @AuthenticationManager = SandboxedModule.require modulePath, requires:
  14. "../../models/User": User: @User = {}
  15. "../../infrastructure/mongojs":
  16. db: @db =
  17. users: {}
  18. ObjectId: ObjectId
  19. "bcrypt": @bcrypt = {}
  20. "settings-sharelatex": @settings
  21. "../V1/V1Handler": @V1Handler = {}
  22. "../User/UserGetter": @UserGetter = {}
  23. @callback = sinon.stub()
  24. describe "with real bcrypt", ->
  25. beforeEach ->
  26. bcrypt = require('bcrypt')
  27. @bcrypt.compare = bcrypt.compare
  28. @bcrypt.getRounds = bcrypt.getRounds
  29. @bcrypt.genSalt = bcrypt.genSalt
  30. @bcrypt.hash = bcrypt.hash
  31. # Hash of 'testpassword'
  32. @testPassword = '$2a$12$zhtThy3R5tLtw5sCwr5XD.zhPENGn4ecjeMcP87oYSYrIICFqBpei'
  33. describe "authenticate", ->
  34. beforeEach ->
  35. @user =
  36. _id: "user-id"
  37. email: @email = "USER@sharelatex.com"
  38. @User.findOne = sinon.stub().callsArgWith(1, null, @user)
  39. describe "when the hashed password matches", ->
  40. beforeEach (done) ->
  41. @unencryptedPassword = "testpassword"
  42. @user.hashedPassword = @testPassword
  43. @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
  44. @callback(error, user)
  45. done()
  46. it "should look up the correct user in the database", ->
  47. @User.findOne.calledWith(email: @email).should.equal true
  48. it "should return the user", ->
  49. @callback.calledWith(null, @user).should.equal true
  50. describe "when the encrypted passwords do not match", ->
  51. beforeEach ->
  52. @AuthenticationManager.authenticate(email: @email, "notthecorrectpassword", @callback)
  53. it "should not return the user", ->
  54. @callback.calledWith(null, null).should.equal true
  55. describe "setUserPasswordInV2", ->
  56. beforeEach ->
  57. @user =
  58. _id: "5c8791477192a80b5e76ca7e"
  59. email: @email = "USER@sharelatex.com"
  60. @db.users.update = sinon.stub().callsArgWith(2, null, {nModified: 1})
  61. it "should not produce an error", (done) ->
  62. @AuthenticationManager.setUserPasswordInV2 @user._id, "testpassword", (err, updated) =>
  63. expect(err).to.not.exist
  64. expect(updated).to.equal true
  65. done()
  66. it "should set the hashed password", (done) ->
  67. @AuthenticationManager.setUserPasswordInV2 @user._id, "testpassword", (err, updated) =>
  68. expect(err).to.not.exist
  69. hashedPassword = @db.users.update.lastCall.args[1].$set.hashedPassword
  70. expect(hashedPassword).to.exist
  71. expect(hashedPassword.length).to.equal 60
  72. expect(hashedPassword).to.match /^\$2a\$12\$[a-zA-Z0-9\/.]{53}$/
  73. done()
  74. describe "authenticate", ->
  75. describe "when the user exists in the database", ->
  76. beforeEach ->
  77. @user =
  78. _id: "user-id"
  79. email: @email = "USER@sharelatex.com"
  80. @unencryptedPassword = "banana"
  81. @User.findOne = sinon.stub().callsArgWith(1, null, @user)
  82. describe "when the hashed password matches", ->
  83. beforeEach (done) ->
  84. @user.hashedPassword = @hashedPassword = "asdfjadflasdf"
  85. @bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  86. @bcrypt.getRounds = sinon.stub().returns 12
  87. @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
  88. @callback(error, user)
  89. done()
  90. it "should look up the correct user in the database", ->
  91. @User.findOne.calledWith(email: @email).should.equal true
  92. it "should check that the passwords match", ->
  93. @bcrypt.compare
  94. .calledWith(@unencryptedPassword, @hashedPassword)
  95. .should.equal true
  96. it "should return the user", ->
  97. @callback.calledWith(null, @user).should.equal true
  98. describe "when the encrypted passwords do not match", ->
  99. beforeEach ->
  100. @AuthenticationManager.authenticate(email: @email, @unencryptedPassword, @callback)
  101. it "should not return the user", ->
  102. @callback.calledWith(null, null).should.equal true
  103. describe "when the hashed password matches but the number of rounds is too low", ->
  104. beforeEach (done) ->
  105. @user.hashedPassword = @hashedPassword = "asdfjadflasdf"
  106. @bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  107. @bcrypt.getRounds = sinon.stub().returns 7
  108. @AuthenticationManager.setUserPassword = sinon.stub().callsArgWith(2, null)
  109. @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
  110. @callback(error, user)
  111. done()
  112. it "should look up the correct user in the database", ->
  113. @User.findOne.calledWith(email: @email).should.equal true
  114. it "should check that the passwords match", ->
  115. @bcrypt.compare
  116. .calledWith(@unencryptedPassword, @hashedPassword)
  117. .should.equal true
  118. it "should check the number of rounds", ->
  119. @bcrypt.getRounds.called.should.equal true
  120. it "should set the users password (with a higher number of rounds)", ->
  121. @AuthenticationManager.setUserPassword
  122. .calledWith("user-id", @unencryptedPassword)
  123. .should.equal true
  124. it "should return the user", ->
  125. @callback.calledWith(null, @user).should.equal true
  126. describe "when the hashed password matches but the number of rounds is too low, but upgrades disabled", ->
  127. beforeEach (done) ->
  128. @settings.security.disableBcryptRoundsUpgrades = true
  129. @user.hashedPassword = @hashedPassword = "asdfjadflasdf"
  130. @bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  131. @bcrypt.getRounds = sinon.stub().returns 7
  132. @AuthenticationManager.setUserPassword = sinon.stub().callsArgWith(2, null)
  133. @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
  134. @callback(error, user)
  135. done()
  136. it "should not check the number of rounds", ->
  137. @bcrypt.getRounds.called.should.equal false
  138. it "should not set the users password (with a higher number of rounds)", ->
  139. @AuthenticationManager.setUserPassword
  140. .calledWith("user-id", @unencryptedPassword)
  141. .should.equal false
  142. it "should return the user", ->
  143. @callback.calledWith(null, @user).should.equal true
  144. describe "when the user does not exist in the database", ->
  145. beforeEach ->
  146. @User.findOne = sinon.stub().callsArgWith(1, null, null)
  147. @AuthenticationManager.authenticate(email: @email, @unencrpytedPassword, @callback)
  148. it "should not return a user", ->
  149. @callback.calledWith(null, null).should.equal true
  150. describe "validateEmail", ->
  151. describe "valid", ->
  152. it "should return null", ->
  153. result = @AuthenticationManager.validateEmail 'foo@example.com'
  154. expect(result).to.equal null
  155. describe "invalid", ->
  156. it "should return validation error object for no email", ->
  157. result = @AuthenticationManager.validateEmail ''
  158. expect(result).to.not.equal null
  159. expect(result.message).to.equal 'email not valid'
  160. it "should return validation error object for invalid", ->
  161. result = @AuthenticationManager.validateEmail 'notanemail'
  162. expect(result).to.not.equal null
  163. expect(result.message).to.equal 'email not valid'
  164. describe "validatePassword", ->
  165. beforeEach ->
  166. # 73 characters:
  167. @longPassword = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345678'
  168. describe "with a null password", ->
  169. it "should return an error", ->
  170. expect(@AuthenticationManager.validatePassword()).to.eql { message: 'password not set' }
  171. describe "password length", ->
  172. describe "with the default password length options", ->
  173. it "should reject passwords that are too short", ->
  174. expect(@AuthenticationManager.validatePassword('')).to.eql { message: 'password is too short' }
  175. expect(@AuthenticationManager.validatePassword('foo')).to.eql { message: 'password is too short' }
  176. it "should reject passwords that are too long", ->
  177. expect(@AuthenticationManager.validatePassword(@longPassword)).to.eql { message: 'password is too long' }
  178. it "should accept passwords that are a good length", ->
  179. expect(@AuthenticationManager.validatePassword('l337h4x0r')).to.equal null
  180. describe "when the password length is specified in settings", ->
  181. beforeEach ->
  182. @settings.passwordStrengthOptions =
  183. length:
  184. min: 10
  185. max: 12
  186. it "should reject passwords that are too short", ->
  187. expect(@AuthenticationManager.validatePassword('012345678')).to.eql { message: 'password is too short' }
  188. it "should accept passwords of exactly minimum length", ->
  189. expect(@AuthenticationManager.validatePassword('0123456789')).to.equal null
  190. it "should reject passwords that are too long", ->
  191. expect(@AuthenticationManager.validatePassword('0123456789abc')).to.eql { message: 'password is too long' }
  192. it "should accept passwords of exactly maximum length", ->
  193. expect(@AuthenticationManager.validatePassword('0123456789ab')).to.equal null
  194. describe "when the maximum password length is set to >72 characters in settings", ->
  195. beforeEach ->
  196. @settings.passwordStrengthOptions =
  197. length:
  198. max: 128
  199. it "should still reject passwords > 72 characters in length", ->
  200. expect(@AuthenticationManager.validatePassword(@longPassword)).to.eql { message: 'password is too long' }
  201. describe "allowed characters", ->
  202. describe "with the default settings for allowed characters", ->
  203. it "should allow passwords with valid characters", ->
  204. expect(@AuthenticationManager.validatePassword("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")).to.equal null
  205. expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.equal null
  206. it "should not allow passwords with invalid characters", ->
  207. expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.eql { message: 'password contains an invalid character' }
  208. describe "when valid characters are overridden in settings", ->
  209. beforeEach ->
  210. @settings.passwordStrengthOptions =
  211. chars:
  212. symbols: " "
  213. it "should allow passwords with valid characters", ->
  214. expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.equal null
  215. it "should disallow passwords with invalid characters", ->
  216. expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.eql { message: 'password contains an invalid character' }
  217. describe "when allowAnyChars is set", ->
  218. beforeEach ->
  219. @settings.passwordStrengthOptions =
  220. allowAnyChars: true
  221. it "should allow any characters", ->
  222. expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.equal null
  223. expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.equal null
  224. describe "setUserPassword", ->
  225. beforeEach ->
  226. @user_id = ObjectId()
  227. @password = "banana"
  228. @hashedPassword = "asdkjfa;osiuvandf"
  229. @salt = "saltaasdfasdfasdf"
  230. @bcrypt.genSalt = sinon.stub().callsArgWith(2, null, @salt)
  231. @bcrypt.hash = sinon.stub().callsArgWith(2, null, @hashedPassword)
  232. @db.users.update = sinon.stub().callsArg(2)
  233. describe "too long", ->
  234. beforeEach ->
  235. @settings.passwordStrengthOptions =
  236. length:
  237. max:10
  238. @password = "dsdsadsadsadsadsadkjsadjsadjsadljs"
  239. it "should return and error", (done)->
  240. @AuthenticationManager.setUserPassword @user_id, @password, (err)->
  241. expect(err).to.exist
  242. done()
  243. it "should not start the bcrypt process", (done)->
  244. @AuthenticationManager.setUserPassword @user_id, @password, (err)=>
  245. @bcrypt.genSalt.called.should.equal false
  246. @bcrypt.hash.called.should.equal false
  247. done()
  248. describe "too short", ->
  249. beforeEach ->
  250. @settings.passwordStrengthOptions =
  251. length:
  252. max:10
  253. min:6
  254. @password = "dsd"
  255. it "should return and error", (done)->
  256. @AuthenticationManager.setUserPassword @user_id, @password, (err)->
  257. expect(err).to.exist
  258. done()
  259. it "should not start the bcrypt process", (done)->
  260. @AuthenticationManager.setUserPassword @user_id, @password, (err)=>
  261. @bcrypt.genSalt.called.should.equal false
  262. @bcrypt.hash.called.should.equal false
  263. done()
  264. describe "password set attempt", ->
  265. describe "with SL user in SL", ->
  266. beforeEach ->
  267. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
  268. @AuthenticationManager.setUserPassword(@user_id, @password, @callback)
  269. it 'should look up the user', ->
  270. @UserGetter.getUser.calledWith(@user_id).should.equal true
  271. it "should update the user's password in the database", ->
  272. args = @db.users.update.lastCall.args
  273. expect(args[0]).to.deep.equal {_id: ObjectId(@user_id.toString())}
  274. expect(args[1]).to.deep.equal {
  275. $set: {
  276. "hashedPassword": @hashedPassword
  277. }
  278. $unset: password: true
  279. }
  280. it "should hash the password", ->
  281. @bcrypt.genSalt
  282. .calledWith(12)
  283. .should.equal true
  284. @bcrypt.hash
  285. .calledWith(@password, @salt)
  286. .should.equal true
  287. it "should call the callback", ->
  288. @callback.called.should.equal true
  289. describe "with SL user in v2", ->
  290. beforeEach (done) ->
  291. @settings.overleaf = true
  292. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
  293. @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
  294. @callback(err, changed)
  295. done()
  296. it "should error", ->
  297. @callback.calledWith(new Errors.SLInV2Error("Password Reset Attempt")).should.equal true
  298. describe "with v2 user in SL", ->
  299. beforeEach (done) ->
  300. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} })
  301. @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
  302. @callback(err, changed)
  303. done()
  304. it "should error", ->
  305. @callback.calledWith(new Errors.NotInV2Error("Password Reset Attempt")).should.equal true
  306. describe "with v2 user in v2", ->
  307. beforeEach (done) ->
  308. @settings.overleaf = true
  309. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} })
  310. @V1Handler.doPasswordReset = sinon.stub().yields(null, true)
  311. @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
  312. @callback(err, changed)
  313. done()
  314. it "should set the password in v2", ->
  315. @callback.calledWith(null, true).should.equal true