Merge pull request #3892 from overleaf/sk-reroll-csrf

Regenerate CSRF token on login

GitOrigin-RevId: 501582b34794a822f4c9fe3af2575b5756511e06
This commit is contained in:
Shane Kilkelly 2021-04-09 09:44:26 +01:00 committed by Copybot
parent 16d02c9d8b
commit 04fa863f9f
7 changed files with 80 additions and 30 deletions

View file

@ -489,7 +489,7 @@ function _afterLoginSessionSetup(req, user, callback) {
// transferred to the new session below. // transferred to the new session below.
for (let key in oldSession) { for (let key in oldSession) {
const value = oldSession[key] const value = oldSession[key]
if (key !== '__tmp') { if (key !== '__tmp' && key !== 'csrfSecret') {
req.session[key] = value req.session[key] = value
} }
} }

View file

@ -8,6 +8,43 @@ describe('Authentication', function() {
user = new User() user = new User()
}) })
describe('CSRF regeneration on login', function() {
it('should prevent use of csrf token from before login', function(done) {
user.logout(err => {
if (err) {
return done(err)
}
user.getCsrfToken(err => {
if (err) {
return done(err)
}
const oldToken = user.csrfToken
user.login(err => {
if (err) {
return done(err)
}
expect(oldToken === user.csrfToken).to.equal(false)
user.request.post(
{
headers: {
'x-csrf-token': oldToken
},
url: '/project/new',
json: { projectName: 'test' }
},
(err, response, body) => {
expect(err).to.not.exist
expect(response.statusCode).to.equal(403)
expect(body).to.equal('Forbidden')
done()
}
)
})
})
})
})
})
describe('login', function() { describe('login', function() {
beforeEach('doLogin', async function() { beforeEach('doLogin', async function() {
await user.login() await user.login()

View file

@ -89,15 +89,20 @@ const tryFollowInviteLink = (user, link, callback) => {
} }
const tryAcceptInvite = (user, invite, callback) => { const tryAcceptInvite = (user, invite, callback) => {
user.request.post( user.getCsrfToken(err => {
{ if (err) {
uri: `/project/${invite.projectId}/invite/token/${invite.token}/accept`, return callback(err)
json: { }
token: invite.token user.request.post(
} {
}, uri: `/project/${invite.projectId}/invite/token/${invite.token}/accept`,
callback json: {
) token: invite.token
}
},
callback
)
})
} }
const tryRegisterUser = (user, email, callback) => { const tryRegisterUser = (user, email, callback) => {

View file

@ -160,22 +160,10 @@ describe('UserHelper', function() {
}) })
describe('getCsrfToken', function() { describe('getCsrfToken', function() {
describe('when the csrfToken is not cached', function() { it('should fetch csrfToken', async function() {
it('should fetch csrfToken', async function() { const userHelper = new UserHelper()
const userHelper = new UserHelper() await userHelper.getCsrfToken()
await userHelper.getCsrfToken() expect(userHelper.csrfToken).to.be.a.string
expect(userHelper.csrfToken).to.be.a.string
})
})
describe('when the csrfToken is cached', function() {
it('should return cached csrfToken', async function() {
let userHelper = new UserHelper()
await userHelper.getCsrfToken()
const csrfToken = userHelper._csrfToken
await userHelper.getCsrfToken()
expect(csrfToken).to.equal(userHelper._csrfToken)
})
}) })
}) })

View file

@ -113,7 +113,18 @@ class User {
url: settings.enableLegacyLogin ? '/login/legacy' : '/login', url: settings.enableLegacyLogin ? '/login/legacy' : '/login',
json: { email, password: this.password } json: { email, password: this.password }
}, },
callback (error, response, body) => {
if (error != null) {
return callback(error)
}
// get new csrf token, then return result of login
this.getCsrfToken(err => {
if (err) {
return callback(err)
}
callback(null, response, body)
})
}
) )
}) })
}) })

View file

@ -123,9 +123,6 @@ class UserHelper {
* Requests csrf token unless already cached in internal state * Requests csrf token unless already cached in internal state
*/ */
async getCsrfToken() { async getCsrfToken() {
if (this._csrfToken) {
return
}
// get csrf token from api and store // get csrf token from api and store
const response = await this.request.get('/dev/csrf') const response = await this.request.get('/dev/csrf')
this._csrfToken = response.body this._csrfToken = response.body
@ -255,6 +252,7 @@ class UserHelper {
if (!userHelper.user) { if (!userHelper.user) {
throw new Error(`user not found for email: ${userData.email}`) throw new Error(`user not found for email: ${userData.email}`)
} }
await userHelper.getCsrfToken()
return userHelper return userHelper
} }
@ -298,6 +296,7 @@ class UserHelper {
if (!userHelper.user) { if (!userHelper.user) {
throw new Error(`user not found for email: ${userData.email}`) throw new Error(`user not found for email: ${userData.email}`)
} }
await userHelper.getCsrfToken()
return userHelper return userHelper
} }

View file

@ -1236,6 +1236,16 @@ describe('AuthenticationController', function() {
this.req.login.callCount.should.equal(1) this.req.login.callCount.should.equal(1)
}) })
it('should erase the CSRF secret', function() {
this.AuthenticationController.finishLogin(
this.user,
this.req,
this.res,
this.next
)
expect(this.req.session.csrfSecret).to.not.exist
})
it('should call req.session.save', function() { it('should call req.session.save', function() {
this.AuthenticationController.finishLogin( this.AuthenticationController.finishLogin(
this.user, this.user,