AuthenticationManager.coffee 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. Settings = require "settings-sharelatex"
  2. User = require("../../models/User").User
  3. {db, ObjectId} = require("../../infrastructure/mongojs")
  4. crypto = require 'crypto'
  5. bcrypt = require 'bcrypt'
  6. EmailHelper = require("../Helpers/EmailHelper")
  7. Errors = require("../Errors/Errors")
  8. UserGetter = require("../User/UserGetter")
  9. V1Handler = require '../V1/V1Handler'
  10. BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12
  11. _checkWriteResult = (result, callback = (error, updated) ->) ->
  12. # for MongoDB
  13. if result and result.nModified == 1
  14. callback(null, true)
  15. else
  16. callback(null, false)
  17. module.exports = AuthenticationManager =
  18. authenticate: (query, password, callback = (error, user) ->) ->
  19. # Using Mongoose for legacy reasons here. The returned User instance
  20. # gets serialized into the session and there may be subtle differences
  21. # between the user returned by Mongoose vs mongojs (such as default values)
  22. User.findOne query, (error, user) =>
  23. return callback(error) if error?
  24. if user?
  25. if user.hashedPassword?
  26. bcrypt.compare password, user.hashedPassword, (error, match) ->
  27. return callback(error) if error?
  28. if match
  29. AuthenticationManager.checkRounds user, user.hashedPassword, password, (err) ->
  30. return callback(err) if err?
  31. callback null, user
  32. else
  33. callback null, null
  34. else
  35. callback null, null
  36. else
  37. callback null, null
  38. validateEmail: (email) ->
  39. parsed = EmailHelper.parseEmail(email)
  40. if !parsed?
  41. return { message: 'email not valid' }
  42. return null
  43. # validates a password based on a similar set of rules to `complexPassword.js` on the frontend
  44. # note that `passfield.js` enforces more rules than this, but these are the most commonly set.
  45. # returns null on success, or an error string.
  46. validatePassword: (password) ->
  47. return { message: 'password not set' } unless password?
  48. allowAnyChars = Settings.passwordStrengthOptions?.allowAnyChars == true
  49. min = Settings.passwordStrengthOptions?.length?.min || 6
  50. max = Settings.passwordStrengthOptions?.length?.max || 72
  51. # we don't support passwords > 72 characters in length, because bcrypt truncates them
  52. max = 72 if max > 72
  53. return { message: 'password is too short' } unless password.length >= min
  54. return { message: 'password is too long' } unless password.length <= max
  55. return { message: 'password contains an invalid character' } unless allowAnyChars || AuthenticationManager._passwordCharactersAreValid(password)
  56. return null
  57. setUserPassword: (user_id, password, callback = (error, changed) ->) ->
  58. validation = @validatePassword(password)
  59. return callback(validation.message) if validation?
  60. UserGetter.getUser user_id, { email:1, overleaf: 1 }, (error, user) ->
  61. return callback(error) if error?
  62. v1IdExists = user.overleaf?.id?
  63. if v1IdExists and Settings.overleaf? # v2 user in v2
  64. # v2 user in v2, change password in v1
  65. AuthenticationManager.setUserPasswordInV1(user.overleaf.id, password, callback)
  66. else if v1IdExists and !Settings.overleaf?
  67. # v2 user in SL
  68. return callback(new Errors.NotInV2Error("Password Reset Attempt"))
  69. else if !v1IdExists and !Settings.overleaf?
  70. # SL user in SL, change password in SL
  71. AuthenticationManager.setUserPasswordInV2(user_id, password, callback)
  72. else if !v1IdExists and Settings.overleaf?
  73. # SL user in v2, should not happen
  74. return callback(new Errors.SLInV2Error("Password Reset Attempt"))
  75. else
  76. return callback(new Error("Password Reset Attempt Failed"))
  77. checkRounds: (user, hashedPassword, password, callback = (error) ->) ->
  78. # Temporarily disable this function, TODO: re-enable this
  79. if Settings?.security?.disableBcryptRoundsUpgrades
  80. return callback()
  81. # check current number of rounds and rehash if necessary
  82. currentRounds = bcrypt.getRounds hashedPassword
  83. if currentRounds < BCRYPT_ROUNDS
  84. AuthenticationManager.setUserPassword user._id, password, callback
  85. else
  86. callback()
  87. setUserPasswordInV2: (user_id, password, callback) ->
  88. validation = @validatePassword(password)
  89. return callback(validation.message) if validation?
  90. minorVersion = 'a'
  91. bcrypt.genSalt BCRYPT_ROUNDS, minorVersion, (error, salt) ->
  92. return callback(error) if error?
  93. bcrypt.hash password, salt, (error, hash) ->
  94. return callback(error) if error?
  95. db.users.update({
  96. _id: ObjectId(user_id.toString())
  97. }, {
  98. $set: hashedPassword: hash
  99. $unset: password: true
  100. }, (updateError, result)->
  101. return callback(updateError) if updateError?
  102. _checkWriteResult(result, callback)
  103. )
  104. setUserPasswordInV1: (v1_user_id, password, callback) ->
  105. validation = @validatePassword(password)
  106. return callback(validation.message) if validation?
  107. V1Handler.doPasswordReset v1_user_id, password, (error, reset)->
  108. return callback(error) if error?
  109. return callback(error, reset)
  110. _passwordCharactersAreValid: (password) ->
  111. digits = Settings.passwordStrengthOptions?.chars?.digits || '1234567890'
  112. letters = Settings.passwordStrengthOptions?.chars?.letters || 'abcdefghijklmnopqrstuvwxyz'
  113. letters_up = Settings.passwordStrengthOptions?.chars?.letters_up || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  114. symbols = Settings.passwordStrengthOptions?.chars?.symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,'
  115. for charIndex in [0..password.length - 1]
  116. return false unless digits.indexOf(password[charIndex]) > -1 or
  117. letters.indexOf(password[charIndex]) > -1 or
  118. letters_up.indexOf(password[charIndex]) > -1 or
  119. symbols.indexOf(password[charIndex]) > -1
  120. return true