Merge pull request #4112 from overleaf/tm-private-api-basic-auth

Add requireBasicAuth middleware and refactor httpAuth to use it

GitOrigin-RevId: 7f68c0dc4a40102bfe4a97711def517e465ec7fd
This commit is contained in:
Jakob Ackermann 2021-05-31 10:30:04 +02:00 committed by Copybot
parent d6454a84bb
commit 95c83866c5
6 changed files with 81 additions and 50 deletions

View file

@ -16,7 +16,7 @@ module.exports = {
publicApiRouter.use( publicApiRouter.use(
'/analytics/uniExternalCollaboration', '/analytics/uniExternalCollaboration',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
AnalyticsProxy.call('/uniExternalCollaboration') AnalyticsProxy.call('/uniExternalCollaboration')
) )
}, },

View file

@ -316,7 +316,7 @@ const AuthenticationController = {
} }
if (req.headers.authorization != null) { if (req.headers.authorization != null) {
AuthenticationController.httpAuth(req, res, next) AuthenticationController.requirePrivateApiAuth()(req, res, next)
} else if (AuthenticationController.isUserLoggedIn(req)) { } else if (AuthenticationController.isUserLoggedIn(req)) {
next() next()
} else { } else {
@ -361,17 +361,23 @@ const AuthenticationController = {
return next() return next()
}, },
httpAuth: basicAuth(function (user, pass) { requireBasicAuth: function (userDetails) {
const expectedPassword = Settings.httpAuthUsers[user] return basicAuth(function (user, pass) {
const isValid = const expectedPassword = userDetails[user]
expectedPassword && const isValid =
expectedPassword.length === pass.length && expectedPassword &&
crypto.timingSafeEqual(Buffer.from(expectedPassword), Buffer.from(pass)) expectedPassword.length === pass.length &&
if (!isValid) { crypto.timingSafeEqual(Buffer.from(expectedPassword), Buffer.from(pass))
logger.err({ user, pass }, 'invalid login details') if (!isValid) {
} logger.err({ user, pass }, 'invalid login details')
return isValid }
}), return isValid
})
},
requirePrivateApiAuth() {
return AuthenticationController.requireBasicAuth(Settings.httpAuthUsers)
},
setRedirectInSession(req, value) { setRedirectInSession(req, value) {
if (value == null) { if (value == null) {

View file

@ -57,7 +57,7 @@ module.exports = {
) )
apiRouter.post( apiRouter.post(
'/project/:Project_id/doc/:entity_id/convert-to-file', '/project/:Project_id/doc/:entity_id/convert-to-file',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
validate({ validate({
body: Joi.object({ body: Joi.object({
userId: Joi.objectId().required(), userId: Joi.objectId().required(),
@ -71,7 +71,7 @@ module.exports = {
// whenever a user joins a project, like updating the deleted status. // whenever a user joins a project, like updating the deleted status.
apiRouter.post( apiRouter.post(
'/project/:Project_id/join', '/project/:Project_id/join',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
RateLimiterMiddleware.rateLimit({ RateLimiterMiddleware.rateLimit({
endpointName: 'join-project', endpointName: 'join-project',
params: ['Project_id'], params: ['Project_id'],

View file

@ -136,7 +136,7 @@ module.exports = {
// Currently used in acceptance tests only, as a way to trigger the syncing logic // Currently used in acceptance tests only, as a way to trigger the syncing logic
return publicApiRouter.post( return publicApiRouter.post(
'/user/:user_id/features/sync', '/user/:user_id/features/sync',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
SubscriptionController.refreshUserFeatures SubscriptionController.refreshUserFeatures
) )
}, },

View file

@ -231,7 +231,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
) )
privateApiRouter.get( privateApiRouter.get(
'/user/:user_id/personal_info', '/user/:user_id/personal_info',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
UserInfoController.getPersonalInfo UserInfoController.getPersonalInfo
) )
@ -564,7 +564,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
) )
privateApiRouter.post( privateApiRouter.post(
'/project/:Project_id/history/resync', '/project/:Project_id/history/resync',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
HistoryController.resyncProjectHistory HistoryController.resyncProjectHistory
) )
@ -639,28 +639,28 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
) )
privateApiRouter.post( privateApiRouter.post(
'/internal/expire-deleted-projects-after-duration', '/internal/expire-deleted-projects-after-duration',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
ProjectController.expireDeletedProjectsAfterDuration ProjectController.expireDeletedProjectsAfterDuration
) )
privateApiRouter.post( privateApiRouter.post(
'/internal/expire-deleted-users-after-duration', '/internal/expire-deleted-users-after-duration',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
UserController.expireDeletedUsersAfterDuration UserController.expireDeletedUsersAfterDuration
) )
privateApiRouter.post( privateApiRouter.post(
'/internal/project/:projectId/expire-deleted-project', '/internal/project/:projectId/expire-deleted-project',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
ProjectController.expireDeletedProject ProjectController.expireDeletedProject
) )
privateApiRouter.post( privateApiRouter.post(
'/internal/users/:userId/expire', '/internal/users/:userId/expire',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
UserController.expireDeletedUser UserController.expireDeletedUser
) )
privateApiRouter.get( privateApiRouter.get(
'/user/:userId/tag', '/user/:userId/tag',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
TagsController.apiGetAllTags TagsController.apiGetAllTags
) )
webRouter.get( webRouter.get(
@ -733,35 +733,35 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
// Deprecated in favour of /internal/project/:project_id but still used by versioning // Deprecated in favour of /internal/project/:project_id but still used by versioning
privateApiRouter.get( privateApiRouter.get(
'/project/:project_id/details', '/project/:project_id/details',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
ProjectApiController.getProjectDetails ProjectApiController.getProjectDetails
) )
// New 'stable' /internal API end points // New 'stable' /internal API end points
privateApiRouter.get( privateApiRouter.get(
'/internal/project/:project_id', '/internal/project/:project_id',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
ProjectApiController.getProjectDetails ProjectApiController.getProjectDetails
) )
privateApiRouter.get( privateApiRouter.get(
'/internal/project/:Project_id/zip', '/internal/project/:Project_id/zip',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
ProjectDownloadsController.downloadProject ProjectDownloadsController.downloadProject
) )
privateApiRouter.get( privateApiRouter.get(
'/internal/project/:project_id/compile/pdf', '/internal/project/:project_id/compile/pdf',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
CompileController.compileAndDownloadPdf CompileController.compileAndDownloadPdf
) )
privateApiRouter.post( privateApiRouter.post(
'/internal/deactivateOldProjects', '/internal/deactivateOldProjects',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
InactiveProjectController.deactivateOldProjects InactiveProjectController.deactivateOldProjects
) )
privateApiRouter.post( privateApiRouter.post(
'/internal/project/:project_id/deactivate', '/internal/project/:project_id/deactivate',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
InactiveProjectController.deactivateProject InactiveProjectController.deactivateProject
) )
@ -775,40 +775,40 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
req.params = params req.params = params
next() next()
}, },
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
CompileController.getFileFromClsi CompileController.getFileFromClsi
) )
privateApiRouter.get( privateApiRouter.get(
'/project/:Project_id/doc/:doc_id', '/project/:Project_id/doc/:doc_id',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
DocumentController.getDocument DocumentController.getDocument
) )
privateApiRouter.post( privateApiRouter.post(
'/project/:Project_id/doc/:doc_id', '/project/:Project_id/doc/:doc_id',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
DocumentController.setDocument DocumentController.setDocument
) )
privateApiRouter.post( privateApiRouter.post(
'/user/:user_id/update/*', '/user/:user_id/update/*',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
TpdsController.mergeUpdate TpdsController.mergeUpdate
) )
privateApiRouter.delete( privateApiRouter.delete(
'/user/:user_id/update/*', '/user/:user_id/update/*',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
TpdsController.deleteUpdate TpdsController.deleteUpdate
) )
privateApiRouter.post( privateApiRouter.post(
'/project/:project_id/contents/*', '/project/:project_id/contents/*',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
TpdsController.updateProjectContents TpdsController.updateProjectContents
) )
privateApiRouter.delete( privateApiRouter.delete(
'/project/:project_id/contents/*', '/project/:project_id/contents/*',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
TpdsController.deleteProjectContents TpdsController.deleteProjectContents
) )
@ -884,7 +884,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
// overleaf-integration module), but may expand beyond that role. // overleaf-integration module), but may expand beyond that role.
publicApiRouter.post( publicApiRouter.post(
'/api/clsi/compile/:submission_id', '/api/clsi/compile/:submission_id',
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
CompileController.compileSubmission CompileController.compileSubmission
) )
publicApiRouter.get( publicApiRouter.get(
@ -898,7 +898,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
req.params = params req.params = params
next() next()
}, },
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
CompileController.getFileFromClsiWithoutUser CompileController.getFileFromClsiWithoutUser
) )
publicApiRouter.post( publicApiRouter.post(
@ -908,7 +908,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
maxRequests: 1, maxRequests: 1,
timeInterval: 60, timeInterval: 60,
}), }),
AuthenticationController.httpAuth, AuthenticationController.requirePrivateApiAuth(),
InstitutionsController.confirmDomain InstitutionsController.confirmDomain
) )

View file

@ -630,7 +630,10 @@ describe('AuthenticationController', function () {
describe('requireGlobalLogin', function () { describe('requireGlobalLogin', function () {
beforeEach(function () { beforeEach(function () {
this.req.headers = {} this.req.headers = {}
this.AuthenticationController.httpAuth = sinon.stub() this.middleware = sinon.stub()
this.AuthenticationController.requirePrivateApiAuth = sinon
.stub()
.returns(this.middleware)
this.setRedirect = sinon.spy( this.setRedirect = sinon.spy(
this.AuthenticationController, this.AuthenticationController,
'setRedirectInSession' 'setRedirectInSession'
@ -684,8 +687,8 @@ describe('AuthenticationController', function () {
) )
}) })
it('should pass the request onto httpAuth', function () { it('should pass the request onto requirePrivateApiAuth middleware', function () {
this.AuthenticationController.httpAuth this.middleware
.calledWith(this.req, this.res, this.next) .calledWith(this.req, this.res, this.next)
.should.equal(true) .should.equal(true)
}) })
@ -726,7 +729,16 @@ describe('AuthenticationController', function () {
}) })
}) })
describe('httpAuth', function () { describe('requireBasicAuth', function () {
beforeEach(function () {
this.basicAuthUsers = {
'basic-auth-user': 'basic-auth-password',
}
this.middleware = this.AuthenticationController.requireBasicAuth(
this.basicAuthUsers
)
})
describe('with http auth', function () { describe('with http auth', function () {
it('should error with incorrect user', function (done) { it('should error with incorrect user', function (done) {
this.req.headers = { this.req.headers = {
@ -736,12 +748,12 @@ describe('AuthenticationController', function () {
expect(status).to.equal('Unauthorized') expect(status).to.equal('Unauthorized')
done() done()
} }
this.AuthenticationController.httpAuth(this.req, this.req) this.middleware(this.req, this.req)
}) })
it('should error with incorrect password', function (done) { it('should error with incorrect password', function (done) {
this.req.headers = { this.req.headers = {
authorization: `Basic ${Buffer.from('valid-test-user:nope').toString( authorization: `Basic ${Buffer.from('basic-auth-user:nope').toString(
'base64' 'base64'
)}`, )}`,
} }
@ -749,12 +761,12 @@ describe('AuthenticationController', function () {
expect(status).to.equal('Unauthorized') expect(status).to.equal('Unauthorized')
done() done()
} }
this.AuthenticationController.httpAuth(this.req, this.req) this.middleware(this.req, this.req)
}) })
it('should fail with empty pass', function (done) { it('should fail with empty pass', function (done) {
this.req.headers = { this.req.headers = {
authorization: `Basic ${Buffer.from(`invalid-test-user:`).toString( authorization: `Basic ${Buffer.from(`basic-auth-user:`).toString(
'base64' 'base64'
)}`, )}`,
} }
@ -762,20 +774,33 @@ describe('AuthenticationController', function () {
expect(status).to.equal('Unauthorized') expect(status).to.equal('Unauthorized')
done() done()
} }
this.AuthenticationController.httpAuth(this.req, this.req) this.middleware(this.req, this.req)
}) })
it('should succeed with correct user/pass', function (done) { it('should succeed with correct user/pass', function (done) {
this.req.headers = { this.req.headers = {
authorization: `Basic ${Buffer.from( authorization: `Basic ${Buffer.from(
`valid-test-user:${this.httpAuthUsers['valid-test-user']}` `basic-auth-user:${this.basicAuthUsers['basic-auth-user']}`
).toString('base64')}`, ).toString('base64')}`,
} }
this.AuthenticationController.httpAuth(this.req, this.res, done) this.middleware(this.req, this.res, done)
}) })
}) })
}) })
describe('requirePrivateApiAuth', function () {
beforeEach(function () {
this.AuthenticationController.requireBasicAuth = sinon.stub()
})
it('should call requireBasicAuth with the private API user details', function () {
this.AuthenticationController.requirePrivateApiAuth()
this.AuthenticationController.requireBasicAuth
.calledWith(this.httpAuthUsers)
.should.equal(true)
})
})
describe('_redirectToLoginOrRegisterPage', function () { describe('_redirectToLoginOrRegisterPage', function () {
beforeEach(function () { beforeEach(function () {
this.middleware = this.AuthenticationController.requireLogin( this.middleware = this.AuthenticationController.requireLogin(