overleaf/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee
Shane Kilkelly 0aaeb6671e Keep password reset token in session, and strip it from reset page url.
This fixes an issue where the reset token was leaked in the referrer header
when navigating away from the password reset page to an external site.

Now we get the token from the query string, store it in the session,
then redirect to the bare url of the password reset page, which then
uses the stored token to render the reset form.
2015-08-24 11:53:33 +01:00

168 lines
5.7 KiB
CoffeeScript

should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/PasswordReset/PasswordResetController"
expect = require("chai").expect
describe "PasswordResetController", ->
beforeEach ->
@settings = {}
@PasswordResetHandler =
generateAndEmailResetToken:sinon.stub()
setNewUserPassword:sinon.stub()
@RateLimiter =
addCount: sinon.stub()
@PasswordResetController = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"./PasswordResetHandler":@PasswordResetHandler
"logger-sharelatex": log:->
"../../infrastructure/RateLimiter":@RateLimiter
@email = "bob@bob.com "
@token = "my security token that was emailed to me"
@password = "my new password"
@req =
body:
email:@email
passwordResetToken:@token
password:@password
i18n:
translate:->
session: {}
query: {}
@res = {}
describe "requestReset", ->
it "should error if the rate limit is hit", (done)->
@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true)
@RateLimiter.addCount.callsArgWith(1, null, false)
@res.send = (code)=>
code.should.equal 500
@PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.trim()).should.equal false
done()
@PasswordResetController.requestReset @req, @res
it "should tell the handler to process that email", (done)->
@RateLimiter.addCount.callsArgWith(1, null, true)
@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true)
@res.sendStatus = (code)=>
code.should.equal 200
@PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.trim()).should.equal true
done()
@PasswordResetController.requestReset @req, @res
it "should send a 500 if there is an error", (done)->
@RateLimiter.addCount.callsArgWith(1, null, true)
@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, "error")
@res.send = (code)=>
code.should.equal 500
done()
@PasswordResetController.requestReset @req, @res
it "should send a 404 if the email doesn't exist", (done)->
@RateLimiter.addCount.callsArgWith(1, null, true)
@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, false)
@res.send = (code)=>
code.should.equal 404
done()
@PasswordResetController.requestReset @req, @res
it "should lowercase the email address", (done)->
@email = "UPerCaseEMAIL@example.Com"
@req.body.email = @email
@RateLimiter.addCount.callsArgWith(1, null, true)
@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true)
@res.sendStatus = (code)=>
code.should.equal 200
@PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.toLowerCase()).should.equal true
done()
@PasswordResetController.requestReset @req, @res
describe "setNewUserPassword", ->
beforeEach ->
@req.session.resetToken = @token
it "should tell the user handler to reset the password", (done)->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true)
@res.sendStatus = (code)=>
code.should.equal 200
@PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true
done()
@PasswordResetController.setNewUserPassword @req, @res
it "should send 404 if the token didn't work", (done)->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, false)
@res.send = (code)=>
code.should.equal 404
done()
@PasswordResetController.setNewUserPassword @req, @res
it "should return 400 (Bad Request) if there is no password", (done)->
@req.body.password = ""
@PasswordResetHandler.setNewUserPassword.callsArgWith(2)
@res.sendStatus = (code)=>
code.should.equal 400
@PasswordResetHandler.setNewUserPassword.called.should.equal false
done()
@PasswordResetController.setNewUserPassword @req, @res
it "should return 400 (Bad Request) if there is no passwordResetToken", (done)->
@req.body.passwordResetToken = ""
@PasswordResetHandler.setNewUserPassword.callsArgWith(2)
@res.sendStatus = (code)=>
code.should.equal 400
@PasswordResetHandler.setNewUserPassword.called.should.equal false
done()
@PasswordResetController.setNewUserPassword @req, @res
it "should clear the session.resetToken", (done) ->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true)
@res.sendStatus = (code)=>
code.should.equal 200
@req.session.should.not.have.property 'resetToken'
done()
@PasswordResetController.setNewUserPassword @req, @res
describe "renderSetPasswordForm", ->
describe "with token in query-string", ->
beforeEach ->
@req.query.passwordResetToken = @token
it "should set session.resetToken and redirect", (done) ->
@req.session.should.not.have.property 'resetToken'
@res.redirect = (path) =>
path.should.equal '/user/password/set'
@req.session.resetToken.should.equal @token
done()
@PasswordResetController.renderSetPasswordForm(@req, @res)
describe "without a token in query-string", ->
describe "with token in session", ->
beforeEach ->
@req.session.resetToken = @token
it "should render the page, passing the reset token", (done) ->
@res.render = (template_path, options) =>
options.passwordResetToken.should.equal @req.session.resetToken
done()
@PasswordResetController.renderSetPasswordForm(@req, @res)
describe "without a token in session", ->
it "should redirect to the reset request page", (done) ->
@res.redirect = (path) =>
path.should.equal "/user/password/reset"
@req.session.should.not.have.property 'resetToken'
done()
@PasswordResetController.renderSetPasswordForm(@req, @res)