Merge pull request #6457 from overleaf/jpa-harden-login
[web] harden login process GitOrigin-RevId: 5c0b7cc725efd5e3e879067ad8a42fe46a47b60d
This commit is contained in:
@@ -7,6 +7,7 @@ const tk = require('timekeeper')
|
||||
const MockRequest = require('../helpers/MockRequest')
|
||||
const MockResponse = require('../helpers/MockResponse')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const AuthenticationErrors = require('../../../../app/src/Features/Authentication/AuthenticationErrors')
|
||||
|
||||
describe('AuthenticationController', function () {
|
||||
beforeEach(function () {
|
||||
@@ -32,6 +33,7 @@ describe('AuthenticationController', function () {
|
||||
|
||||
this.AuthenticationController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'./AuthenticationErrors': AuthenticationErrors,
|
||||
'../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
|
||||
addEntry: sinon.stub().yields(null),
|
||||
}),
|
||||
@@ -63,6 +65,7 @@ describe('AuthenticationController', function () {
|
||||
'@overleaf/settings': (this.Settings = {
|
||||
siteUrl: 'http://www.foo.bar',
|
||||
httpAuthUsers: this.httpAuthUsers,
|
||||
elevateAccountSecurityAfterFailedLogin: 90 * 24 * 60 * 60 * 1000,
|
||||
}),
|
||||
passport: (this.passport = {
|
||||
authenticate: sinon.stub().returns(sinon.stub()),
|
||||
@@ -254,7 +257,7 @@ describe('AuthenticationController', function () {
|
||||
this.next
|
||||
)
|
||||
this.res.json.callCount.should.equal(1)
|
||||
this.res.json.calledWith({ message: this.info }).should.equal(true)
|
||||
this.res.json.should.have.been.calledWith({ message: this.info })
|
||||
expect(this.res.json.lastCall.args[0].redir != null).to.equal(false)
|
||||
})
|
||||
})
|
||||
@@ -273,6 +276,7 @@ describe('AuthenticationController', function () {
|
||||
postLoginRedirect: '/path/to/redir/to',
|
||||
},
|
||||
}
|
||||
this.req.__authAuditInfo = { captcha: 'disabled' }
|
||||
this.cb = sinon.stub()
|
||||
})
|
||||
|
||||
@@ -325,22 +329,103 @@ describe('AuthenticationController', function () {
|
||||
.stub()
|
||||
.callsArgWith(2, null, this.user)
|
||||
this.req.sessionID = Math.random()
|
||||
this.AuthenticationController.doPassportLogin(
|
||||
this.req,
|
||||
this.req.body.email,
|
||||
this.req.body.password,
|
||||
this.cb
|
||||
)
|
||||
})
|
||||
|
||||
it('should attempt to authorise the user', function () {
|
||||
this.AuthenticationManager.authenticate
|
||||
.calledWith({ email: this.email.toLowerCase() }, this.password)
|
||||
.should.equal(true)
|
||||
describe('happy path', function () {
|
||||
beforeEach(function () {
|
||||
this.AuthenticationController.doPassportLogin(
|
||||
this.req,
|
||||
this.req.body.email,
|
||||
this.req.body.password,
|
||||
this.cb
|
||||
)
|
||||
})
|
||||
it('should attempt to authorise the user', function () {
|
||||
this.AuthenticationManager.authenticate
|
||||
.calledWith({ email: this.email.toLowerCase() }, this.password)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it("should establish the user's session", function () {
|
||||
this.cb.calledWith(null, this.user).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("should establish the user's session", function () {
|
||||
this.cb.calledWith(null, this.user).should.equal(true)
|
||||
describe('when authenticate flags a parallel login', function () {
|
||||
beforeEach(function () {
|
||||
this.AuthenticationManager.authenticate = sinon
|
||||
.stub()
|
||||
.callsArgWith(2, new AuthenticationErrors.ParallelLoginError())
|
||||
this.AuthenticationController.doPassportLogin(
|
||||
this.req,
|
||||
this.req.body.email,
|
||||
this.req.body.password,
|
||||
this.cb
|
||||
)
|
||||
})
|
||||
|
||||
it('should send a 429', function () {
|
||||
this.cb.should.have.been.calledWith(null, false, { status: 429 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a user having a recent failed login ', function () {
|
||||
beforeEach(function () {
|
||||
this.user.lastFailedLogin = new Date()
|
||||
})
|
||||
|
||||
describe('with captcha disabled', function () {
|
||||
beforeEach(function () {
|
||||
this.req.__authAuditInfo.captcha = 'disabled'
|
||||
this.AuthenticationController.doPassportLogin(
|
||||
this.req,
|
||||
this.req.body.email,
|
||||
this.req.body.password,
|
||||
this.cb
|
||||
)
|
||||
})
|
||||
|
||||
it('should let the user log in', function () {
|
||||
this.cb.should.have.been.calledWith(null, this.user)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a solved captcha', function () {
|
||||
beforeEach(function () {
|
||||
this.req.__authAuditInfo.captcha = 'solved'
|
||||
this.AuthenticationController.doPassportLogin(
|
||||
this.req,
|
||||
this.req.body.email,
|
||||
this.req.body.password,
|
||||
this.cb
|
||||
)
|
||||
})
|
||||
|
||||
it('should let the user log in', function () {
|
||||
this.cb.should.have.been.calledWith(null, this.user)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a skipped captcha', function () {
|
||||
beforeEach(function () {
|
||||
this.req.__authAuditInfo.captcha = 'skipped'
|
||||
this.AuthenticationController.doPassportLogin(
|
||||
this.req,
|
||||
this.req.body.email,
|
||||
this.req.body.password,
|
||||
this.cb
|
||||
)
|
||||
})
|
||||
|
||||
it('should request a captcha', function () {
|
||||
this.cb.should.have.been.calledWith(null, false, {
|
||||
text: 'cannot_verify_user_not_robot',
|
||||
type: 'error',
|
||||
errorReason: 'cannot_verify_user_not_robot',
|
||||
status: 400,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -3,17 +3,21 @@ const { expect } = require('chai')
|
||||
const SandboxedModule = require('sandboxed-module')
|
||||
const { ObjectId } = require('mongodb')
|
||||
const AuthenticationErrors = require('../../../../app/src/Features/Authentication/AuthenticationErrors')
|
||||
const tk = require('timekeeper')
|
||||
|
||||
const modulePath =
|
||||
'../../../../app/src/Features/Authentication/AuthenticationManager.js'
|
||||
|
||||
describe('AuthenticationManager', function () {
|
||||
beforeEach(function () {
|
||||
tk.freeze(Date.now())
|
||||
this.settings = { security: { bcryptRounds: 4 } }
|
||||
this.AuthenticationManager = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'../../models/User': {
|
||||
User: (this.User = {}),
|
||||
User: (this.User = {
|
||||
updateOne: sinon.stub().callsArgWith(3, null, { nModified: 1 }),
|
||||
}),
|
||||
},
|
||||
'../../infrastructure/mongodb': {
|
||||
db: (this.db = { users: {} }),
|
||||
@@ -31,6 +35,10 @@ describe('AuthenticationManager', function () {
|
||||
this.callback = sinon.stub()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
tk.reset()
|
||||
})
|
||||
|
||||
describe('with real bcrypt', function () {
|
||||
beforeEach(function () {
|
||||
const bcrypt = require('bcrypt')
|
||||
@@ -49,13 +57,13 @@ describe('AuthenticationManager', function () {
|
||||
_id: 'user-id',
|
||||
email: (this.email = 'USER@sharelatex.com'),
|
||||
}
|
||||
this.user.hashedPassword = this.testPassword
|
||||
this.User.findOne = sinon.stub().callsArgWith(1, null, this.user)
|
||||
})
|
||||
|
||||
describe('when the hashed password matches', function () {
|
||||
beforeEach(function (done) {
|
||||
this.unencryptedPassword = 'testpassword'
|
||||
this.user.hashedPassword = this.testPassword
|
||||
this.AuthenticationManager.authenticate(
|
||||
{ email: this.email },
|
||||
this.unencryptedPassword,
|
||||
@@ -70,17 +78,46 @@ describe('AuthenticationManager', function () {
|
||||
this.User.findOne.calledWith({ email: this.email }).should.equal(true)
|
||||
})
|
||||
|
||||
it('should bump epoch', function () {
|
||||
this.User.updateOne.should.have.been.calledWith(
|
||||
{
|
||||
_id: this.user._id,
|
||||
loginEpoch: this.user.loginEpoch,
|
||||
},
|
||||
{
|
||||
$inc: { loginEpoch: 1 },
|
||||
},
|
||||
{}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the user', function () {
|
||||
this.callback.calledWith(null, this.user).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the encrypted passwords do not match', function () {
|
||||
beforeEach(function () {
|
||||
beforeEach(function (done) {
|
||||
this.AuthenticationManager.authenticate(
|
||||
{ email: this.email },
|
||||
'notthecorrectpassword',
|
||||
this.callback
|
||||
(...args) => {
|
||||
this.callback(...args)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should persist the login failure and bump epoch', function () {
|
||||
this.User.updateOne.should.have.been.calledWith(
|
||||
{
|
||||
_id: this.user._id,
|
||||
loginEpoch: this.user.loginEpoch,
|
||||
},
|
||||
{
|
||||
$inc: { loginEpoch: 1 },
|
||||
$set: { lastFailedLogin: new Date() },
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -88,6 +125,52 @@ describe('AuthenticationManager', function () {
|
||||
this.callback.calledWith(null, null).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when another request runs in parallel', function () {
|
||||
beforeEach(function () {
|
||||
this.User.updateOne = sinon
|
||||
.stub()
|
||||
.callsArgWith(3, null, { nModified: 0 })
|
||||
})
|
||||
|
||||
describe('correct password', function () {
|
||||
beforeEach(function (done) {
|
||||
this.AuthenticationManager.authenticate(
|
||||
{ email: this.email },
|
||||
'testpassword',
|
||||
(...args) => {
|
||||
this.callback(...args)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return an error', function () {
|
||||
this.callback.should.have.been.calledWith(
|
||||
sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('bad password', function () {
|
||||
beforeEach(function (done) {
|
||||
this.User.updateOne = sinon.stub().yields(null, { nModified: 0 })
|
||||
this.AuthenticationManager.authenticate(
|
||||
{ email: this.email },
|
||||
'notthecorrectpassword',
|
||||
(...args) => {
|
||||
this.callback(...args)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
it('should return an error', function () {
|
||||
this.callback.should.have.been.calledWith(
|
||||
sinon.match.instanceOf(AuthenticationErrors.ParallelLoginError)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setUserPasswordInV2', function () {
|
||||
|
||||
Reference in New Issue
Block a user