-
Notifications
You must be signed in to change notification settings - Fork 78
Return 401 if authentication is not provided #1488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -11,7 +11,6 @@ | |||||
// defined elsewhere into an actual Express service. The only thing it needs in | ||||||
// order to do this is a valid dependency injection context container. | ||||||
const { match } = require('path-to-regexp'); | ||||||
const { without } = require('ramda'); | ||||||
|
||||||
module.exports = (container) => { | ||||||
const service = require('express')(); | ||||||
|
@@ -64,9 +63,9 @@ module.exports = (container) => { | |||||
const { builder } = require('./endpoint'); | ||||||
const { emptyAuthInjector, authHandler, queryOptionsHandler, userAgentHandler } = require('./preprocessors'); | ||||||
|
||||||
const preprocessors = [ emptyAuthInjector, authHandler, queryOptionsHandler, userAgentHandler ]; | ||||||
const endpoint = builder(container, preprocessors); | ||||||
const anonymousEndpoint = builder(container, without([authHandler], preprocessors)); | ||||||
const preprocessors = [ queryOptionsHandler, userAgentHandler ]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
const endpoint = builder(container, [authHandler, ...preprocessors]); | ||||||
const anonymousEndpoint = builder(container, [emptyAuthInjector, ...preprocessors]); | ||||||
|
||||||
|
||||||
//////////////////////////////////////////////////////////////////////////////// | ||||||
|
@@ -75,10 +74,10 @@ module.exports = (container) => { | |||||
require('../resources/app-users')(service, endpoint); | ||||||
require('../resources/odata')(service, endpoint); | ||||||
require('../resources/odata-entities')(service, endpoint); | ||||||
require('../resources/forms')(service, endpoint); | ||||||
require('../resources/users')(service, endpoint); | ||||||
require('../resources/forms')(service, endpoint, anonymousEndpoint); | ||||||
require('../resources/users')(service, endpoint, anonymousEndpoint); | ||||||
require('../resources/sessions')(service, endpoint, anonymousEndpoint); | ||||||
require('../resources/submissions')(service, endpoint); | ||||||
require('../resources/submissions')(service, endpoint, anonymousEndpoint); | ||||||
require('../resources/config')(service, endpoint); | ||||||
require('../resources/projects')(service, endpoint); | ||||||
require('../resources/roles')(service, endpoint); | ||||||
|
@@ -90,7 +89,7 @@ module.exports = (container) => { | |||||
require('../resources/analytics')(service, endpoint); | ||||||
require('../resources/datasets')(service, endpoint); | ||||||
require('../resources/entities')(service, endpoint); | ||||||
require('../resources/oidc')(service, endpoint); | ||||||
require('../resources/oidc')(service, endpoint, anonymousEndpoint); | ||||||
require('../resources/user-preferences')(service, endpoint); | ||||||
|
||||||
//////////////////////////////////////////////////////////////////////////////// | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,12 +65,14 @@ const findMultipart = (files) => { | |
}; | ||
const forceAuthFailed = Problem.translate(Problem.user.insufficientRights, Problem.user.authenticationFailed); | ||
|
||
module.exports = (service, endpoint) => { | ||
module.exports = (service, endpoint, anonymousEndpoint) => { | ||
|
||
//////////////////////////////////////////////////////////////////////////////// | ||
// SUBMISSIONS (OPENROSA) | ||
|
||
const openRosaSubmission = (path, draft, getForm) => { | ||
const endpointByTestPath = path.startsWith('/test/:key') ? anonymousEndpoint : endpoint; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this code be more intuitive if the e.g. const openRosaSubmission = (endpoint, path, draft, getForm) => { or const openRosaSubmission = (path, { isDraft, requiresAuth }, getForm) => { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of making this an explicit argument (either of the suggestions above). 👍 |
||
|
||
// Nonstandard REST; OpenRosa-specific API. | ||
// This bit of silliness is to address the fact that the OpenRosa standards | ||
// specification requires the presence of a HEAD /submission endpoint, but | ||
|
@@ -81,9 +83,10 @@ module.exports = (service, endpoint) => { | |
// there is no body content, and the HTTP spec requires that HEAD should | ||
// return exactly what GET would, just without a response body, the only thing | ||
// we can possibly respond with in either case is no body and a 204 code. | ||
service.get(path, endpoint.openRosa(({ Projects }, { params }) => | ||
service.get(path, endpointByTestPath.openRosa(({ Projects, Forms }, { params, auth }) => | ||
Projects.getById(params.projectId) | ||
.then(getOrNotFound) | ||
.then(() => (params.xmlFormId ? getForm(auth, params, params.xmlFormId, Forms) : resolve())) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not obvious how this change is related to |
||
.then(always({ code: 204, body: '' })))); | ||
|
||
// Temporary solution to https://github.com/expressjs/multer/issues/1104 | ||
|
@@ -101,7 +104,7 @@ module.exports = (service, endpoint) => { | |
}; | ||
|
||
// Nonstandard REST; OpenRosa-specific API. | ||
service.post(path, multipart.any(), multerUtf, multipartErrorHandler, endpoint.openRosa(({ Forms, Submissions, SubmissionAttachments }, { params, files, auth, query, userAgent }) => | ||
service.post(path, multipart.any(), multerUtf, multipartErrorHandler, endpointByTestPath.openRosa(({ Forms, Submissions, SubmissionAttachments }, { params, files, auth, query, userAgent }) => | ||
Submission.fromXml(findMultipart(files).buffer) | ||
.then((partial) => getForm(auth, params, partial.xmlFormId, Forms, partial.def.version) | ||
.catch(forceAuthFailed) | ||
|
@@ -267,7 +270,7 @@ module.exports = (service, endpoint) => { | |
); | ||
|
||
// Create Submission using draftToken | ||
service.post(`/test/:key/projects/:projectId/forms/:formId/draft/submissions`, endpoint(({ Forms, Submissions, SubmissionAttachments }, { params, query, userAgent }, request) => | ||
service.post(`/test/:key/projects/:projectId/forms/:formId/draft/submissions`, anonymousEndpoint(({ Forms, Submissions, SubmissionAttachments }, { params, query, userAgent }, request) => | ||
Submission.fromXml(request) | ||
.then((partial) => Forms.getByProjectAndXmlFormId(params.projectId, params.formId, false, Form.DraftVersion) | ||
.then(getOrNotFound) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the GET /users endpoint, could you remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or maybe it's a nice safeguard? It sort of feels redundant to me now. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,7 @@ const { resolve, reject, getOrNotFound } = require('../util/promise'); | |
const { isPresent } = require('../util/util'); | ||
const oidc = require('../util/oidc'); | ||
|
||
module.exports = (service, endpoint) => { | ||
module.exports = (service, endpoint, anonymousEndpoint) => { | ||
|
||
// Get a list of user accounts. | ||
// TODO: paging. | ||
|
@@ -54,8 +54,11 @@ module.exports = (service, endpoint) => { | |
.then((token) => mail(savedUser.email, 'accountCreated', { token }))) | ||
.then(always(savedUser))))); | ||
|
||
const endpointByInvalidateQuery = (cb) => (req, ...rest) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe it'd be reasonable to do something like that now? What if Just a thought, I'm also happy with the approach here. 👍 |
||
(req.query.invalidate ? endpoint(cb) : anonymousEndpoint(cb))(req, ...rest); | ||
|
||
// TODO/SECURITY: subtle timing attack here. | ||
service.post('/users/reset/initiate', endpoint(({ Users, mail }, { auth, body, query }) => | ||
service.post('/users/reset/initiate', endpointByInvalidateQuery(({ Users, mail }, { auth, body, query }) => | ||
(!body.email ? Problem.user.missingParameter({ field: 'email' }) : Users.getByEmail(body.email) | ||
.then((maybeUser) => maybeUser | ||
.map((user) => ((isTrue(query.invalidate)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,10 +4,10 @@ const testData = require('../../data/xml'); | |
|
||
describe('api: /projects/:id/app-users', () => { | ||
describe('POST', () => { | ||
it('should return 403 unless the user is allowed to create', testService((service) => | ||
it('should return 403 if unauthenticated', testService((service) => | ||
service.post('/v1/projects/1/app-users') | ||
.send({ displayName: 'test1' }) | ||
.expect(403))); | ||
.expect(401))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would there be value in a corresponding test case for authenticated users who are unauthorised? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar question from me. I'd expect every endpoint that calls |
||
|
||
it('should return the created key', testService((service) => | ||
service.login('alice', (asAlice) => | ||
|
@@ -63,8 +63,8 @@ describe('api: /projects/:id/app-users', () => { | |
}); | ||
|
||
describe('GET', () => { | ||
it('should return 403 unless the user is allowed to list', testService((service) => | ||
service.get('/v1/projects/1/app-users').expect(403))); | ||
it('should return 401 if unauthenticated', testService((service) => | ||
service.get('/v1/projects/1/app-users').expect(401))); | ||
|
||
it('should return a list of tokens in order with merged data', testService((service) => | ||
service.login('alice', (asAlice) => | ||
|
@@ -174,11 +174,11 @@ describe('api: /projects/:id/app-users', () => { | |
}); | ||
|
||
describe('/:id DELETE', () => { | ||
it('should return 403 unless the user can delete', testService((service) => | ||
it('should return 401 if unauthenticated', testService((service) => | ||
service.login('alice', (asAlice) => | ||
asAlice.post('/v1/projects/1/app-users').send({ displayName: 'condemned' }).expect(200) | ||
.then(({ body }) => | ||
service.delete('/v1/projects/1/app-users/' + body.id).expect(403))))); | ||
service.delete('/v1/projects/1/app-users/' + body.id).expect(401))))); | ||
|
||
it('should delete the token', testService((service) => | ||
service.login('alice', (asAlice) => | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is an "empty" session now handled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the callback passed to
authBySessionToken()
handles that case.