AuthenticationManagerTests.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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 "authenticate", ->
  25. describe "when the user exists in the database", ->
  26. beforeEach ->
  27. @user =
  28. _id: "user-id"
  29. email: @email = "USER@sharelatex.com"
  30. @unencryptedPassword = "banana"
  31. @User.findOne = sinon.stub().callsArgWith(1, null, @user)
  32. describe "when the hashed password matches", ->
  33. beforeEach (done) ->
  34. @user.hashedPassword = @hashedPassword = "asdfjadflasdf"
  35. @bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  36. @bcrypt.getRounds = sinon.stub().returns 12
  37. @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
  38. @callback(error, user)
  39. done()
  40. it "should look up the correct user in the database", ->
  41. @User.findOne.calledWith(email: @email).should.equal true
  42. it "should check that the passwords match", ->
  43. @bcrypt.compare
  44. .calledWith(@unencryptedPassword, @hashedPassword)
  45. .should.equal true
  46. it "should return the user", ->
  47. @callback.calledWith(null, @user).should.equal true
  48. describe "when the encrypted passwords do not match", ->
  49. beforeEach ->
  50. @AuthenticationManager._encryptPassword = sinon.stub().returns("Not the encrypted password")
  51. @AuthenticationManager.authenticate(email: @email, @unencryptedPassword, @callback)
  52. it "should not return the user", ->
  53. @callback.calledWith(null, null).should.equal true
  54. describe "when the hashed password matches but the number of rounds is too low", ->
  55. beforeEach (done) ->
  56. @user.hashedPassword = @hashedPassword = "asdfjadflasdf"
  57. @bcrypt.compare = sinon.stub().callsArgWith(2, null, true)
  58. @bcrypt.getRounds = sinon.stub().returns 7
  59. @AuthenticationManager.setUserPassword = sinon.stub().callsArgWith(2, null)
  60. @AuthenticationManager.authenticate email: @email, @unencryptedPassword, (error, user) =>
  61. @callback(error, user)
  62. done()
  63. it "should look up the correct user in the database", ->
  64. @User.findOne.calledWith(email: @email).should.equal true
  65. it "should check that the passwords match", ->
  66. @bcrypt.compare
  67. .calledWith(@unencryptedPassword, @hashedPassword)
  68. .should.equal true
  69. it "should check the number of rounds", ->
  70. @bcrypt.getRounds.called.should.equal true
  71. it "should set the users password (with a higher number of rounds)", ->
  72. @AuthenticationManager.setUserPassword
  73. .calledWith("user-id", @unencryptedPassword)
  74. .should.equal true
  75. it "should return the user", ->
  76. @callback.calledWith(null, @user).should.equal true
  77. describe "when the user does not exist in the database", ->
  78. beforeEach ->
  79. @User.findOne = sinon.stub().callsArgWith(1, null, null)
  80. @AuthenticationManager.authenticate(email: @email, @unencrpytedPassword, @callback)
  81. it "should not return a user", ->
  82. @callback.calledWith(null, null).should.equal true
  83. describe "validateEmail", ->
  84. describe "valid", ->
  85. it "should return null", ->
  86. result = @AuthenticationManager.validateEmail 'foo@example.com'
  87. expect(result).to.equal null
  88. describe "invalid", ->
  89. it "should return validation error object for no email", ->
  90. result = @AuthenticationManager.validateEmail ''
  91. expect(result).to.not.equal null
  92. expect(result.message).to.equal 'email not valid'
  93. it "should return validation error object for invalid", ->
  94. result = @AuthenticationManager.validateEmail 'notanemail'
  95. expect(result).to.not.equal null
  96. expect(result.message).to.equal 'email not valid'
  97. describe "validatePassword", ->
  98. beforeEach ->
  99. # 73 characters:
  100. @longPassword = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345678'
  101. describe "with a null password", ->
  102. it "should return an error", ->
  103. expect(@AuthenticationManager.validatePassword()).to.eql { message: 'password not set' }
  104. describe "password length", ->
  105. describe "with the default password length options", ->
  106. it "should reject passwords that are too short", ->
  107. expect(@AuthenticationManager.validatePassword('')).to.eql { message: 'password is too short' }
  108. expect(@AuthenticationManager.validatePassword('foo')).to.eql { message: 'password is too short' }
  109. it "should reject passwords that are too long", ->
  110. expect(@AuthenticationManager.validatePassword(@longPassword)).to.eql { message: 'password is too long' }
  111. it "should accept passwords that are a good length", ->
  112. expect(@AuthenticationManager.validatePassword('l337h4x0r')).to.equal null
  113. describe "when the password length is specified in settings", ->
  114. beforeEach ->
  115. @settings.passwordStrengthOptions =
  116. length:
  117. min: 10
  118. max: 12
  119. it "should reject passwords that are too short", ->
  120. expect(@AuthenticationManager.validatePassword('012345678')).to.eql { message: 'password is too short' }
  121. it "should accept passwords of exactly minimum length", ->
  122. expect(@AuthenticationManager.validatePassword('0123456789')).to.equal null
  123. it "should reject passwords that are too long", ->
  124. expect(@AuthenticationManager.validatePassword('0123456789abc')).to.eql { message: 'password is too long' }
  125. it "should accept passwords of exactly maximum length", ->
  126. expect(@AuthenticationManager.validatePassword('0123456789ab')).to.equal null
  127. describe "when the maximum password length is set to >72 characters in settings", ->
  128. beforeEach ->
  129. @settings.passwordStrengthOptions =
  130. length:
  131. max: 128
  132. it "should still reject passwords > 72 characters in length", ->
  133. expect(@AuthenticationManager.validatePassword(@longPassword)).to.eql { message: 'password is too long' }
  134. describe "allowed characters", ->
  135. describe "with the default settings for allowed characters", ->
  136. it "should allow passwords with valid characters", ->
  137. expect(@AuthenticationManager.validatePassword("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")).to.equal null
  138. expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.equal null
  139. it "should not allow passwords with invalid characters", ->
  140. expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.eql { message: 'password contains an invalid character' }
  141. describe "when valid characters are overridden in settings", ->
  142. beforeEach ->
  143. @settings.passwordStrengthOptions =
  144. chars:
  145. symbols: " "
  146. it "should allow passwords with valid characters", ->
  147. expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.equal null
  148. it "should disallow passwords with invalid characters", ->
  149. expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.eql { message: 'password contains an invalid character' }
  150. describe "when allowAnyChars is set", ->
  151. beforeEach ->
  152. @settings.passwordStrengthOptions =
  153. allowAnyChars: true
  154. it "should allow any characters", ->
  155. expect(@AuthenticationManager.validatePassword("correct horse battery staple")).to.equal null
  156. expect(@AuthenticationManager.validatePassword("1234567890@#$%^&*()-_=+[]{};:<>/?!£€.,")).to.equal null
  157. describe "setUserPassword", ->
  158. beforeEach ->
  159. @user_id = ObjectId()
  160. @password = "banana"
  161. @hashedPassword = "asdkjfa;osiuvandf"
  162. @salt = "saltaasdfasdfasdf"
  163. @bcrypt.genSalt = sinon.stub().callsArgWith(1, null, @salt)
  164. @bcrypt.hash = sinon.stub().callsArgWith(2, null, @hashedPassword)
  165. @db.users.update = sinon.stub().callsArg(2)
  166. describe "too long", ->
  167. beforeEach ->
  168. @settings.passwordStrengthOptions =
  169. length:
  170. max:10
  171. @password = "dsdsadsadsadsadsadkjsadjsadjsadljs"
  172. it "should return and error", (done)->
  173. @AuthenticationManager.setUserPassword @user_id, @password, (err)->
  174. expect(err).to.exist
  175. done()
  176. it "should not start the bcrypt process", (done)->
  177. @AuthenticationManager.setUserPassword @user_id, @password, (err)=>
  178. @bcrypt.genSalt.called.should.equal false
  179. @bcrypt.hash.called.should.equal false
  180. done()
  181. describe "too short", ->
  182. beforeEach ->
  183. @settings.passwordStrengthOptions =
  184. length:
  185. max:10
  186. min:6
  187. @password = "dsd"
  188. it "should return and error", (done)->
  189. @AuthenticationManager.setUserPassword @user_id, @password, (err)->
  190. expect(err).to.exist
  191. done()
  192. it "should not start the bcrypt process", (done)->
  193. @AuthenticationManager.setUserPassword @user_id, @password, (err)=>
  194. @bcrypt.genSalt.called.should.equal false
  195. @bcrypt.hash.called.should.equal false
  196. done()
  197. describe "password set attempt", ->
  198. describe "with SL user in SL", ->
  199. beforeEach ->
  200. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
  201. @AuthenticationManager.setUserPassword(@user_id, @password, @callback)
  202. it 'should look up the user', ->
  203. @UserGetter.getUser.calledWith(@user_id).should.equal true
  204. it "should update the user's password in the database", ->
  205. args = @db.users.update.lastCall.args
  206. expect(args[0]).to.deep.equal {_id: ObjectId(@user_id.toString())}
  207. expect(args[1]).to.deep.equal {
  208. $set: {
  209. "hashedPassword": @hashedPassword
  210. }
  211. $unset: password: true
  212. }
  213. it "should hash the password", ->
  214. @bcrypt.genSalt
  215. .calledWith(12)
  216. .should.equal true
  217. @bcrypt.hash
  218. .calledWith(@password, @salt)
  219. .should.equal true
  220. it "should call the callback", ->
  221. @callback.called.should.equal true
  222. describe "with SL user in v2", ->
  223. beforeEach (done) ->
  224. @settings.overleaf = true
  225. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
  226. @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
  227. @callback(err, changed)
  228. done()
  229. it "should error", ->
  230. @callback.calledWith(new Errors.SLInV2Error("Password Reset Attempt")).should.equal true
  231. describe "with v2 user in SL", ->
  232. beforeEach (done) ->
  233. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} })
  234. @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
  235. @callback(err, changed)
  236. done()
  237. it "should error", ->
  238. @callback.calledWith(new Errors.NotInV2Error("Password Reset Attempt")).should.equal true
  239. describe "with v2 user in v2", ->
  240. beforeEach (done) ->
  241. @settings.overleaf = true
  242. @UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} })
  243. @V1Handler.doPasswordReset = sinon.stub().yields(null, true)
  244. @AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
  245. @callback(err, changed)
  246. done()
  247. it "should set the password in v2", ->
  248. @callback.calledWith(null, true).should.equal true