Merge pull request #1273 from sharelatex/ja-password-reset-v1

Handle v1-only users in v2 password reset flow

GitOrigin-RevId: 38ce8e9aebd3330b980e73640a23661d8015d4f3
This commit is contained in:
Paulo Jorge Reis 2018-12-18 11:14:41 +00:00 committed by sharelatex
parent 4563ce864c
commit e139abb110
7 changed files with 208 additions and 76 deletions

View file

@ -62,21 +62,17 @@ module.exports = AuthenticationManager =
UserGetter.getUser user_id, { email:1, overleaf: 1 }, (error, user) -> UserGetter.getUser user_id, { email:1, overleaf: 1 }, (error, user) ->
return callback(error) if error? return callback(error) if error?
overleafId = user.overleaf?.id? v1IdExists = user.overleaf?.id?
if overleafId and Settings.overleaf? # v2 user in v2 if v1IdExists and Settings.overleaf? # v2 user in v2
# v2 user in v2, change password in v1 # v2 user in v2, change password in v1
AuthenticationManager._setUserPasswordInV1({ AuthenticationManager.setUserPasswordInV1(user.overleaf.id, password, callback)
v1Id: user.overleaf.id, else if v1IdExists and !Settings.overleaf?
email: user.email,
password: password
}, callback)
else if overleafId and !Settings.overleaf?
# v2 user in SL # v2 user in SL
return callback(new Errors.NotInV2Error("Password Reset Attempt")) return callback(new Errors.NotInV2Error("Password Reset Attempt"))
else if !overleafId and !Settings.overleaf? else if !v1IdExists and !Settings.overleaf?
# SL user in SL, change password in SL # SL user in SL, change password in SL
AuthenticationManager._setUserPasswordInV2(user_id, password, callback) AuthenticationManager.setUserPasswordInV2(user_id, password, callback)
else if !overleafId and Settings.overleaf? else if !v1IdExists and Settings.overleaf?
# SL user in v2, should not happen # SL user in v2, should not happen
return callback(new Errors.SLInV2Error("Password Reset Attempt")) return callback(new Errors.SLInV2Error("Password Reset Attempt"))
else else
@ -90,7 +86,10 @@ module.exports = AuthenticationManager =
else else
callback() callback()
_setUserPasswordInV2: (user_id, password, callback) -> setUserPasswordInV2: (user_id, password, callback) ->
validation = @validatePassword(password)
return callback(validation.message) if validation?
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) -> bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
return callback(error) if error? return callback(error) if error?
bcrypt.hash password, salt, (error, hash) -> bcrypt.hash password, salt, (error, hash) ->
@ -105,7 +104,10 @@ module.exports = AuthenticationManager =
_checkWriteResult(result, callback) _checkWriteResult(result, callback)
) )
_setUserPasswordInV1: (user, callback) -> setUserPasswordInV1: (v1_user_id, password, callback) ->
V1Handler.doPasswordReset user, (error, reset)-> validation = @validatePassword(password)
return callback(validation.message) if validation?
V1Handler.doPasswordReset v1_user_id, password, (error, reset)->
return callback(error) if error? return callback(error) if error?
return callback(error, reset) return callback(error, reset)

View file

@ -57,6 +57,7 @@ module.exports =
else if err and !err.statusCode else if err and !err.statusCode
res.status(500) res.status(500)
else if found else if found
return res.sendStatus 200 if !user_id? # will not exist for v1-only users
UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) -> UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) ->
return next(err) if err? return next(err) if err?
if req.body.login_after if req.body.login_after

View file

@ -5,16 +5,15 @@ OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
EmailHandler = require("../Email/EmailHandler") EmailHandler = require("../Email/EmailHandler")
AuthenticationManager = require("../Authentication/AuthenticationManager") AuthenticationManager = require("../Authentication/AuthenticationManager")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
V1Api = require("../V1/V1Api")
module.exports = module.exports = PasswordResetHandler =
generateAndEmailResetToken:(email, callback = (error, exists) ->)-> generateAndEmailResetToken:(email, callback = (error, exists) ->)->
UserGetter.getUserByMainEmail email, (err, user)-> PasswordResetHandler._getPasswordResetData email, (error, exists, data) ->
if err then return callback(err) if error? or !exists
if !user? or user.holdingAccount return callback(error, exists)
logger.err email:email, "user could not be found for password reset" OneTimeTokenHandler.getNewToken 'password', data, (err, token)->
return callback(null, false)
OneTimeTokenHandler.getNewToken 'password', user._id, (err, token)->
if err then return callback(err) if err then return callback(err)
emailOptions = emailOptions =
to : email to : email
@ -24,10 +23,45 @@ module.exports =
callback null, true callback null, true
setNewUserPassword: (token, password, callback = (error, found, user_id) ->)-> setNewUserPassword: (token, password, callback = (error, found, user_id) ->)->
OneTimeTokenHandler.getValueFromTokenAndExpire 'password', token, (err, user_id)-> OneTimeTokenHandler.getValueFromTokenAndExpire 'password', token, (err, data)->
if err then return callback(err) if err then return callback(err)
if !user_id? if !data?
return callback null, false, null return callback null, false, null
AuthenticationManager.setUserPassword user_id, password, (err, reset) -> if typeof data == "string"
# Backwards compatible with old format.
# Tokens expire after 1h, so this can be removed soon after deploy.
# Possibly we should keep this until we do an onsite release too.
data = { user_id: data }
if data.user_id?
AuthenticationManager.setUserPassword data.user_id, password, (err, reset) ->
if err then return callback(err) if err then return callback(err)
callback null, reset, user_id callback null, reset, data.user_id
else if data.v1_user_id?
AuthenticationManager.setUserPasswordInV1 data.v1_user_id, password, (error, reset) ->
return callback(error) if error?
UserGetter.getUser { 'overleaf.id': data.v1_user_id }, {_id:1}, (error, user) ->
return callback(error) if error?
callback null, reset, user?._id
_getPasswordResetData: (email, callback = (error, exists, data) ->) ->
if settings.overleaf?
# Overleaf v2
V1Api.request {
url: "/api/v1/sharelatex/user_emails"
qs:
email: email
expectedStatusCodes: [404]
}, (error, response, body) ->
return callback(error) if error?
if response.statusCode == 404
return callback null, false
else
return callback null, true, { v1_user_id: body.user_id }
else
# ShareLaTeX
UserGetter.getUserByMainEmail email, (err, user)->
if err then return callback(err)
if !user? or user.holdingAccount or user.overleaf?
logger.err email:email, "user could not be found for password reset"
return callback(null, false)
return callback null, true, { user_id: user._id }

View file

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

View file

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

View file

@ -29,6 +29,7 @@ describe "PasswordResetController", ->
"../User/UserSessionsManager": @UserSessionsManager "../User/UserSessionsManager": @UserSessionsManager
@email = "bob@bob.com " @email = "bob@bob.com "
@user_id = 'mock-user-id'
@token = "my security token that was emailed to me" @token = "my security token that was emailed to me"
@password = "my new password" @password = "my new password"
@req = @req =
@ -98,7 +99,7 @@ describe "PasswordResetController", ->
@req.session.resetToken = @token @req.session.resetToken = @token
it "should tell the user handler to reset the password", (done)-> it "should tell the user handler to reset the password", (done)->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true) @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id)
@res.sendStatus = (code)=> @res.sendStatus = (code)=>
code.should.equal 200 code.should.equal 200
@PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true @PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true
@ -106,7 +107,7 @@ describe "PasswordResetController", ->
@PasswordResetController.setNewUserPassword @req, @res @PasswordResetController.setNewUserPassword @req, @res
it "should send 404 if the token didn't work", (done)-> it "should send 404 if the token didn't work", (done)->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, false) @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, false, @user_id)
@res.sendStatus = (code)=> @res.sendStatus = (code)=>
code.should.equal 404 code.should.equal 404
done() done()
@ -131,7 +132,7 @@ describe "PasswordResetController", ->
@PasswordResetController.setNewUserPassword @req, @res @PasswordResetController.setNewUserPassword @req, @res
it "should clear the session.resetToken", (done) -> it "should clear the session.resetToken", (done) ->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true) @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id)
@res.sendStatus = (code)=> @res.sendStatus = (code)=>
code.should.equal 200 code.should.equal 200
@req.session.should.not.have.property 'resetToken' @req.session.should.not.have.property 'resetToken'
@ -139,7 +140,7 @@ describe "PasswordResetController", ->
@PasswordResetController.setNewUserPassword @req, @res @PasswordResetController.setNewUserPassword @req, @res
it 'should clear sessions', (done) -> it 'should clear sessions', (done) ->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true) @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id)
@res.sendStatus = (code)=> @res.sendStatus = (code)=>
@UserSessionsManager.revokeAllUserSessions.callCount.should.equal 1 @UserSessionsManager.revokeAllUserSessions.callCount.should.equal 1
done() done()

View file

@ -17,15 +17,21 @@ describe "PasswordResetHandler", ->
getValueFromTokenAndExpire:sinon.stub() getValueFromTokenAndExpire:sinon.stub()
@UserGetter = @UserGetter =
getUserByMainEmail:sinon.stub() getUserByMainEmail:sinon.stub()
getUser: sinon.stub()
@EmailHandler = @EmailHandler =
sendEmail:sinon.stub() sendEmail:sinon.stub()
@AuthenticationManager = @AuthenticationManager =
setUserPassword:sinon.stub() setUserPassword:sinon.stub()
setUserPasswordInV1:sinon.stub()
setUserPasswordInV2:sinon.stub()
@V1Api =
request: sinon.stub()
@PasswordResetHandler = SandboxedModule.require modulePath, requires: @PasswordResetHandler = SandboxedModule.require modulePath, requires:
"../User/UserGetter": @UserGetter "../User/UserGetter": @UserGetter
"../Security/OneTimeTokenHandler": @OneTimeTokenHandler "../Security/OneTimeTokenHandler": @OneTimeTokenHandler
"../Email/EmailHandler":@EmailHandler "../Email/EmailHandler":@EmailHandler
"../Authentication/AuthenticationManager":@AuthenticationManager "../Authentication/AuthenticationManager":@AuthenticationManager
"../V1/V1Api": @V1Api
"settings-sharelatex": @settings "settings-sharelatex": @settings
"logger-sharelatex": "logger-sharelatex":
log:-> log:->
@ -33,12 +39,13 @@ describe "PasswordResetHandler", ->
@token = "12312321i" @token = "12312321i"
@user_id = "user_id_here" @user_id = "user_id_here"
@user = @user =
email :"bob@bob.com" email : @email = "bob@bob.com"
@password = "my great secret password" @password = "my great secret password"
@callback = sinon.stub()
describe "generateAndEmailResetToken", -> describe "generateAndEmailResetToken", ->
describe "when in ShareLaTeX", ->
it "should check the user exists", (done)-> it "should check the user exists", (done)->
@UserGetter.getUserByMainEmail.callsArgWith(1) @UserGetter.getUserByMainEmail.callsArgWith(1)
@OneTimeTokenHandler.getNewToken.yields() @OneTimeTokenHandler.getNewToken.yields()
@ -46,9 +53,7 @@ describe "PasswordResetHandler", ->
exists.should.equal false exists.should.equal false
done() done()
it "should send the email with the token", (done)-> it "should send the email with the token", (done)->
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user) @UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
@OneTimeTokenHandler.getNewToken.yields(null, @token) @OneTimeTokenHandler.getNewToken.yields(null, @token)
@EmailHandler.sendEmail.callsArgWith(2) @EmailHandler.sendEmail.callsArgWith(2)
@ -68,21 +73,111 @@ describe "PasswordResetHandler", ->
exists.should.equal false exists.should.equal false
done() done()
describe "when in overleaf", ->
beforeEach ->
@settings.overleaf = true
describe "when the email exists", ->
beforeEach ->
@V1Api.request.yields(null, {}, { user_id: 42 })
@OneTimeTokenHandler.getNewToken.yields(null, @token)
@EmailHandler.sendEmail.yields()
@PasswordResetHandler.generateAndEmailResetToken @email, @callback
it 'should call the v1 api for the user', ->
@V1Api.request.calledWith({
url: "/api/v1/sharelatex/user_emails"
qs:
email: @email
expectedStatusCodes: [404]
}).should.equal true
it 'should set the password token data to the user id and email', ->
@OneTimeTokenHandler.getNewToken
.calledWith('password', {
v1_user_id: 42
})
.should.equal true
it 'should send an email with the token', ->
@EmailHandler.sendEmail.called.should.equal true
args = @EmailHandler.sendEmail.args[0]
args[0].should.equal "passwordResetRequested"
args[1].setNewPasswordUrl.should.equal "#{@settings.siteUrl}/user/password/set?passwordResetToken=#{@token}&email=#{encodeURIComponent(@user.email)}"
it 'should return exists == true', ->
@callback.calledWith(null, true).should.equal true
describe "when the email doesn't exist", ->
beforeEach ->
@V1Api.request = sinon.stub().yields(null, { statusCode: 404 }, {})
@PasswordResetHandler.generateAndEmailResetToken @email, @callback
it 'should not set the password token data', ->
@OneTimeTokenHandler.getNewToken
.called.should.equal false
it 'should send an email with the token', ->
@EmailHandler.sendEmail.called.should.equal false
it 'should return exists == false', ->
@callback.calledWith(null, false).should.equal true
describe "setNewUserPassword", -> describe "setNewUserPassword", ->
describe "when no data is found", ->
beforeEach ->
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, null)
@PasswordResetHandler.setNewUserPassword @token, @password, @callback
it "should return false if no user id can be found", (done)-> it 'should return exists == false', ->
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields() @callback.calledWith(null, false).should.equal true
@PasswordResetHandler.setNewUserPassword @token, @password, (err, found) =>
found.should.equal false
@AuthenticationManager.setUserPassword.called.should.equal false
done()
it "should set the user password", (done)-> describe 'when the data is an old style user_id', ->
beforeEach ->
@AuthenticationManager.setUserPassword.yields(null, true, @user_id)
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, @user_id) @OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, @user_id)
@AuthenticationManager.setUserPassword.yields(null, true) @PasswordResetHandler.setNewUserPassword @token, @password, @callback
@PasswordResetHandler.setNewUserPassword @token, @password, (err, found, user_id) =>
found.should.equal true
user_id.should.equal @user_id
@AuthenticationManager.setUserPassword.calledWith(@user_id, @password).should.equal true
done()
it 'should call setUserPasswordInV2', ->
@AuthenticationManager.setUserPassword
.calledWith(@user_id, @password)
.should.equal true
it 'should reset == true and the user_id', ->
@callback.calledWith(null, true, @user_id).should.equal true
describe 'when the data is a new style user_id', ->
beforeEach ->
@AuthenticationManager.setUserPassword.yields(null, true, @user_id)
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, {@user_id})
@PasswordResetHandler.setNewUserPassword @token, @password, @callback
it 'should call setUserPasswordInV2', ->
@AuthenticationManager.setUserPassword
.calledWith(@user_id, @password)
.should.equal true
it 'should reset == true and the user_id', ->
@callback.calledWith(null, true, @user_id).should.equal true
describe 'when the data is v1 id', ->
beforeEach ->
@v1_user_id = 2345
@AuthenticationManager.setUserPasswordInV1.yields(null, true)
@UserGetter.getUser.withArgs({'overleaf.id': @v1_user_id}).yields(null, { _id: @user_id })
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, {@v1_user_id})
@PasswordResetHandler.setNewUserPassword @token, @password, @callback
it 'should call setUserPasswordInV1', ->
@AuthenticationManager.setUserPasswordInV1
.calledWith(@v1_user_id, @password)
.should.equal true
it 'should look up the user by v1 id for the v2 user id', ->
@UserGetter.getUser
.calledWith({'overleaf.id': @v1_user_id})
.should.equal true
it 'should reset == true and the user_id', ->
@callback.calledWith(null, true, @user_id).should.equal true