Merge pull request #1236 from sharelatex/jel-password-reset

Reset password via API request to v1

GitOrigin-RevId: 00b0306ca77df650595a762382a8a63b05a945f6
This commit is contained in:
Jessica Lawshe 2018-12-14 09:45:18 -06:00 committed by sharelatex
parent a72b29efd8
commit 7666c8a481
13 changed files with 187 additions and 45 deletions

View file

@ -4,9 +4,19 @@ User = require("../../models/User").User
crypto = require 'crypto'
bcrypt = require 'bcrypt'
EmailHelper = require("../Helpers/EmailHelper")
Errors = require("../Errors/Errors")
UserGetter = require("../User/UserGetter")
V1Handler = require '../V1/V1Handler'
BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12
_checkWriteResult = (result, callback = (error, updated) ->) ->
# for MongoDB
if result and result.nModified == 1
callback(null, true)
else
callback(null, false)
module.exports = AuthenticationManager =
authenticate: (query, password, callback = (error, user) ->) ->
# Using Mongoose for legacy reasons here. The returned User instance
@ -46,10 +56,41 @@ module.exports = AuthenticationManager =
return { message: 'password is too short' }
return null
setUserPassword: (user_id, password, callback = (error) ->) ->
setUserPassword: (user_id, password, callback = (error, changed) ->) ->
validation = @validatePassword(password)
return callback(validation.message) if validation?
UserGetter.getUser user_id, { email:1, overleaf: 1 }, (error, user) ->
return callback(error) if error?
overleafId = user.overleaf?.id?
if overleafId and Settings.overleaf? # v2 user in v2
# v2 user in v2, change password in v1
AuthenticationManager._setUserPasswordInV1({
v1Id: user.overleaf.id,
email: user.email,
password: password
}, callback)
else if overleafId and !Settings.overleaf?
# v2 user in SL
return callback(new Errors.NotInV2Error("Password Reset Attempt"))
else if !overleafId and !Settings.overleaf?
# SL user in SL, change password in SL
AuthenticationManager._setUserPasswordInV2(user_id, password, callback)
else if !overleafId and Settings.overleaf?
# SL user in v2, should not happen
return callback(new Errors.SLInV2Error("Password Reset Attempt"))
else
return callback(new Error("Password Reset Attempt Failed"))
checkRounds: (user, hashedPassword, password, callback = (error) ->) ->
# check current number of rounds and rehash if necessary
currentRounds = bcrypt.getRounds hashedPassword
if currentRounds < BCRYPT_ROUNDS
AuthenticationManager.setUserPassword user._id, password, callback
else
callback()
_setUserPasswordInV2: (user_id, password, callback) ->
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
return callback(error) if error?
bcrypt.hash password, salt, (error, hash) ->
@ -59,12 +100,12 @@ module.exports = AuthenticationManager =
}, {
$set: hashedPassword: hash
$unset: password: true
}, callback)
}, (updateError, result)->
return callback(updateError) if updateError?
_checkWriteResult(result, callback)
)
checkRounds: (user, hashedPassword, password, callback = (error) ->) ->
# check current number of rounds and rehash if necessary
currentRounds = bcrypt.getRounds hashedPassword
if currentRounds < BCRYPT_ROUNDS
AuthenticationManager.setUserPassword user._id, password, callback
else
callback()
_setUserPasswordInV1: (user, callback) ->
V1Handler.doPasswordReset user, (error, reset)->
return callback(error) if error?
return callback(error, reset)

View file

@ -89,6 +89,20 @@ AccountMergeError = (message) ->
return error
AccountMergeError.prototype.__proto__ = Error.prototype
NotInV2Error = (message) ->
error = new Error(message)
error.name = "NotInV2Error"
error.__proto__ = NotInV2Error.prototype
return error
NotInV2Error.prototype.__proto__ = Error.prototype
SLInV2Error = (message) ->
error = new Error(message)
error.name = "SLInV2Error"
error.__proto__ = SLInV2Error.prototype
return error
SLInV2Error.prototype.__proto__ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError
ServiceNotConfiguredError: ServiceNotConfiguredError
@ -103,3 +117,5 @@ module.exports = Errors =
EmailExistsError: EmailExistsError
InvalidError: InvalidError
AccountMergeError: AccountMergeError
NotInV2Error: NotInV2Error
SLInV2Error: SLInV2Error

View file

@ -46,8 +46,17 @@ module.exports =
return res.sendStatus 400
delete req.session.resetToken
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) ->
return next(err) if err?
if found
if err and err.name and err.name == "NotFoundError"
res.status(404).send("NotFoundError")
else if err and err.name and err.name == "NotInV2Error"
res.status(403).send("NotInV2Error")
else if err and err.name and err.name == "SLInV2Error"
res.status(403).send("SLInV2Error")
else if err and err.statusCode and err.statusCode == 500
res.status(500)
else if err and !err.statusCode
res.status(500)
else if found
UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) ->
return next(err) if err?
if req.body.login_after

View file

@ -28,6 +28,6 @@ module.exports =
if err then return callback(err)
if !user_id?
return callback null, false, null
AuthenticationManager.setUserPassword user_id, password, (err) ->
AuthenticationManager.setUserPassword user_id, password, (err, reset) ->
if err then return callback(err)
callback null, true, user_id
callback null, reset, user_id

View file

@ -25,3 +25,26 @@ module.exports = V1Handler =
else
err = new Error("Unexpected status from v1 login api: #{response.statusCode}")
callback(err)
doPasswordReset: (userData, callback=(err, created)->) ->
logger.log({v1Id: userData.v1Id, email: userData.email},
"sending password reset request to v1 login api")
V1Api.request {
method: 'POST'
url: "/api/v1/sharelatex/reset_password"
json: {
user_id: userData.v1Id,
email: userData.email,
password: userData.password
}
expectedStatusCodes: [200]
}, (err, response, body) ->
if err?
logger.err {email: userData.email, err}, "error while talking to v1 password reset api"
return callback(err, false)
if response.statusCode in [200]
logger.log {email: userData.email, changed: true}, "got success response from v1 password reset api"
callback(null, true)
else
err = new Error("Unexpected status from v1 password reset api: #{response.statusCode}")
callback(err, false)

View file

@ -19,8 +19,21 @@ block content
.alert.alert-success(ng-show="passwordResetForm.response.success")
| #{translate("password_has_been_reset")}.
a(href='/login') #{translate("login_here")}
.alert.alert-danger(ng-show="passwordResetForm.response.error")
| #{translate("password_reset_token_expired")}
div(ng-show="passwordResetForm.response.error == true")
.alert.alert-danger(ng-show="passwordResetForm.response.data == 'NotFoundError'")
| #{translate('password_reset_token_expired')}
br
a(href="/user/password/reset" style="text-decoration: underline;")
| Request a new password reset email
.alert.alert-danger(ng-show="passwordResetForm.response.data == 'SLInV2Error'")
a(href=settings.accountMerge.sharelatexHost + "/user/password/reset")
| Please go to ShareLaTeX to reset your password
.alert.alert-danger(ng-show="passwordResetForm.response.data == 'NotInV2Error'")
a(href=settings.accountMerge.betaHost + "/user/password/reset" style="text-decoration: underline;")
| Please go to Overleaf to reset your password
.alert.alert-danger(ng-show="passwordResetForm.response.status == 500")
| #{translate('error_performing_request')}
.form-group
input.form-control#passwordField(
@ -47,4 +60,4 @@ block content
script(type='text/javascript').
window.usersEmail = "#{getReqQueryParam('email')}"
window.passwordStrengthOptions = !{JSON.stringify(settings.passwordStrengthOptions || {})}
window.passwordStrengthOptions = !{JSON.stringify(settings.passwordStrengthOptions || {})}

View file

@ -256,7 +256,7 @@ block content
div.alert.alert-info
| If you can't remember your password, or if you are using Single-Sign-On with another provider
| to sign in (such as Twitter or Google), please
| #[a(href='/sign_in_to_v1?return_to=/users/password/new', target='_blank') reset your password],
| #[a(href="/user/password/reset", target='_blank') reset your password],
| and try again.
.modal-footer
button.btn.btn-default(
@ -271,4 +271,3 @@ block content
script(type='text/javascript').
window.passwordStrengthOptions = !{JSON.stringify(settings.passwordStrengthOptions || {})}

View file

@ -93,6 +93,8 @@ define(['base', 'libs/passfield'], function(App) {
scope[attrs.name].inflight = false
response.success = false
response.error = true
response.status = status
response.data = data
const onErrorHandler = scope[attrs.onError]
if (onErrorHandler) {

View file

@ -417,6 +417,13 @@ describe 'TokenAccess', ->
, done)
describe 'unimported v1 project', ->
before ->
settings.overleaf =
host: 'http://localhost:5000'
after ->
delete settings.overleaf
it 'should redirect read and write token to v1', (done) ->
unimportedV1Token = '123abc'
try_read_and_write_token_access(@owner, unimportedV1Token, (response, body) =>

View file

@ -70,7 +70,7 @@ class User
options = {upsert: true, new: true, setDefaultsOnInsert: true}
UserModel.findOneAndUpdate filter, {}, options, (error, user) =>
return callback(error) if error?
AuthenticationManager.setUserPassword user._id, @password, (error) =>
AuthenticationManager._setUserPasswordInV2 user._id, @password, (error) =>
return callback(error) if error?
UserUpdater.updateUser user._id, $set: emails: @emails, (error) =>
return callback(error) if error?

View file

@ -126,9 +126,6 @@ module.exports =
path: '/destination/get_and_post'
}
overleaf:
host: "http://overleaf.test:5000"
redirects:
'/redirect/one': '/destination/one',
'/redirect/get_and_post': {

View file

@ -6,6 +6,7 @@ modulePath = "../../../../app/js/Features/Authentication/AuthenticationManager.j
SandboxedModule = require('sandboxed-module')
events = require "events"
ObjectId = require("mongojs").ObjectId
Errors = require "../../../../app/js/Features/Errors/Errors"
describe "AuthenticationManager", ->
beforeEach ->
@ -18,6 +19,8 @@ describe "AuthenticationManager", ->
ObjectId: ObjectId
"bcrypt": @bcrypt = {}
"settings-sharelatex": @settings
"../V1/V1Handler": @V1Handler = {}
"../User/UserGetter": @UserGetter = {}
@callback = sinon.stub()
describe "authenticate", ->
@ -28,7 +31,7 @@ describe "AuthenticationManager", ->
email: @email = "USER@sharelatex.com"
@unencryptedPassword = "banana"
@User.findOne = sinon.stub().callsArgWith(1, null, @user)
describe "when the hashed password matches", ->
beforeEach (done) ->
@user.hashedPassword = @hashedPassword = "asdfjadflasdf"
@ -151,7 +154,7 @@ describe "AuthenticationManager", ->
describe "too long", ->
beforeEach ->
@settings.passwordStrengthOptions =
length:
length:
max:10
@password = "dsdsadsadsadsadsadkjsadjsadjsadljs"
@ -185,30 +188,62 @@ describe "AuthenticationManager", ->
@bcrypt.hash.called.should.equal false
done()
describe "successful set", ->
beforeEach ->
@AuthenticationManager.setUserPassword(@user_id, @password, @callback)
describe "password set attempt", ->
describe "with SL user in SL", ->
beforeEach ->
@UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
@AuthenticationManager.setUserPassword(@user_id, @password, @callback)
it "should update the user's password in the database", ->
args = @db.users.update.lastCall.args
expect(args[0]).to.deep.equal {_id: ObjectId(@user_id.toString())}
expect(args[1]).to.deep.equal {
$set: {
"hashedPassword": @hashedPassword
it 'should look up the user', ->
@UserGetter.getUser.calledWith(@user_id).should.equal true
it "should update the user's password in the database", ->
args = @db.users.update.lastCall.args
expect(args[0]).to.deep.equal {_id: ObjectId(@user_id.toString())}
expect(args[1]).to.deep.equal {
$set: {
"hashedPassword": @hashedPassword
}
$unset: password: true
}
$unset: password: true
}
it "should hash the password", ->
@bcrypt.genSalt
.calledWith(12)
.should.equal true
@bcrypt.hash
.calledWith(@password, @salt)
.should.equal true
it "should hash the password", ->
@bcrypt.genSalt
.calledWith(12)
.should.equal true
@bcrypt.hash
.calledWith(@password, @salt)
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe "with SL user in v2", ->
beforeEach (done) ->
@settings.overleaf = true
@UserGetter.getUser = sinon.stub().yields(null, { overleaf: null })
@AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
@callback(err, changed)
done()
it "should error", ->
@callback.calledWith(new Errors.SLInV2Error("Password Reset Attempt")).should.equal true
describe "with v2 user in SL", ->
beforeEach (done) ->
@UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} })
@AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
@callback(err, changed)
done()
it "should error", ->
@callback.calledWith(new Errors.NotInV2Error("Password Reset Attempt")).should.equal true
describe "with v2 user in v2", ->
beforeEach (done) ->
@settings.overleaf = true
@UserGetter.getUser = sinon.stub().yields(null, { overleaf: {id: 1} })
@V1Handler.doPasswordReset = sinon.stub().yields(null, true)
@AuthenticationManager.setUserPassword @user_id, @password, (err, changed) =>
@callback(err, changed)
done()
it "should set the password in v2", ->
@callback.calledWith(null, true).should.equal true

View file

@ -79,7 +79,7 @@ describe "PasswordResetHandler", ->
it "should set the user password", (done)->
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, @user_id)
@AuthenticationManager.setUserPassword.callsArgWith(2)
@AuthenticationManager.setUserPassword.yields(null, true)
@PasswordResetHandler.setNewUserPassword @token, @password, (err, found, user_id) =>
found.should.equal true
user_id.should.equal @user_id