Skip to content

Commit 733585b

Browse files
committed
Return 401 if authentication is not provided
1 parent 18a956d commit 733585b

16 files changed

+192
-156
lines changed

lib/http/preprocessors.js

+8-11
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,11 @@ const { SESSION_COOKIE } = require('./sessions');
1919
// injects an empty/anonymous auth object into the request context.
2020
const emptyAuthInjector = ({ Auth }, context) => context.with({ auth: Auth.by(null) });
2121

22-
// if one of (Bearer|Basic|Cookie) credentials are provided in the correct conditions
22+
// if one of (FieldKey|Bearer|Basic|Cookie) credentials are provided in the correct conditions
2323
// then authHandler injects the appropriate auth information to the context
2424
// given the appropriate credentials. if credentials are given but don't match a
2525
// session or user, aborts the request with a 401.
2626
//
27-
// otherwise, nothing is done. n.b. this means you must use the emptyAuthInjector
28-
// in conjunction with this function!
29-
//
3027
// TODO?: repetitive, but deduping it makes it even harder to understand.
3128
const authHandler = ({ Sessions, Users, Auth }, context) => {
3229
const authBySessionToken = (token, onFailure = noop) => Sessions.getByBearerToken(token)
@@ -105,14 +102,14 @@ const authHandler = ({ Sessions, Users, Auth }, context) => {
105102
} else if (context.headers.cookie != null) {
106103
// fail the request unless we are under HTTPS.
107104
if ((context.protocol !== 'https') && (context.headers['x-forwarded-proto'] !== 'https'))
108-
return;
105+
return reject(Problem.user.httpsOnly());
109106

110107
// otherwise get the cookie contents.
111108
const token = context.cookies[SESSION_COOKIE];
112109
if (token == null)
113-
return;
114-
// actually try to authenticate with it. no Problem on failure.
115-
const maybeSession = authBySessionToken(token);
110+
return reject(Problem.user.authenticationFailed());
111+
// actually try to authenticate with it. reject on failure.
112+
const maybeSession = authBySessionToken(token, () => reject(Problem.user.authenticationFailed()));
116113

117114
// Following code is for the CSRF protection.
118115
// Rules:
@@ -128,9 +125,6 @@ const authHandler = ({ Sessions, Users, Auth }, context) => {
128125

129126
// if POST run authentication as usual but we'll have to check CSRF afterwards.
130127
return maybeSession.then((cxt) => { // we have to use cxt rather than context for the linter
131-
// if authentication failed anyway, just do nothing.
132-
if ((cxt == null) || cxt.auth.session.isEmpty()) return;
133-
134128
const csrf = urlDecode(cxt.body.__csrf);
135129
if (csrf.isEmpty() || isBlank(csrf.get()) || (cxt.auth.session.get().csrf !== csrf.get())) {
136130
return reject(Problem.user.authenticationFailed());
@@ -140,6 +134,9 @@ const authHandler = ({ Sessions, Users, Auth }, context) => {
140134
// payload expectations.
141135
return cxt.with({ body: omit([ '__csrf' ], cxt.body) });
142136
});
137+
} else {
138+
// Authentication not provided by any means
139+
return reject(Problem.user.authenticationFailed());
143140
}
144141
};
145142

lib/http/service.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
// defined elsewhere into an actual Express service. The only thing it needs in
1212
// order to do this is a valid dependency injection context container.
1313
const { match } = require('path-to-regexp');
14-
const { without } = require('ramda');
1514

1615
module.exports = (container) => {
1716
const service = require('express')();
@@ -64,9 +63,9 @@ module.exports = (container) => {
6463
const { builder } = require('./endpoint');
6564
const { emptyAuthInjector, authHandler, queryOptionsHandler, userAgentHandler } = require('./preprocessors');
6665

67-
const preprocessors = [ emptyAuthInjector, authHandler, queryOptionsHandler, userAgentHandler ];
68-
const endpoint = builder(container, preprocessors);
69-
const anonymousEndpoint = builder(container, without([authHandler], preprocessors));
66+
const preprocessors = [ queryOptionsHandler, userAgentHandler ];
67+
const endpoint = builder(container, [authHandler, ...preprocessors]);
68+
const anonymousEndpoint = builder(container, [emptyAuthInjector, ...preprocessors]);
7069

7170

7271
////////////////////////////////////////////////////////////////////////////////
@@ -75,10 +74,10 @@ module.exports = (container) => {
7574
require('../resources/app-users')(service, endpoint);
7675
require('../resources/odata')(service, endpoint);
7776
require('../resources/odata-entities')(service, endpoint);
78-
require('../resources/forms')(service, endpoint);
79-
require('../resources/users')(service, endpoint);
77+
require('../resources/forms')(service, endpoint, anonymousEndpoint);
78+
require('../resources/users')(service, endpoint, anonymousEndpoint);
8079
require('../resources/sessions')(service, endpoint, anonymousEndpoint);
81-
require('../resources/submissions')(service, endpoint);
80+
require('../resources/submissions')(service, endpoint, anonymousEndpoint);
8281
require('../resources/config')(service, endpoint);
8382
require('../resources/projects')(service, endpoint);
8483
require('../resources/roles')(service, endpoint);

lib/resources/forms.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const streamAttachment = async (container, attachment, response) => {
5858
}
5959
};
6060

61-
module.exports = (service, endpoint) => {
61+
module.exports = (service, endpoint, anonymousEndpoint) => {
6262
// This forms list can also be used to get a list of just the soft-deleted forms by adding ?deleted=true
6363
// TODO: paging.
6464
service.get('/projects/:projectId/forms', endpoint(({ Forms, Projects }, { auth, params, query, queryOptions }) =>
@@ -418,7 +418,7 @@ module.exports = (service, endpoint) => {
418418
noargs(Problem.user.notFound)
419419
);
420420

421-
service.get('/test/:key/projects/:projectId/forms/:id/draft/formList', endpoint.openRosa(({ Forms, FormAttachments, env }, { params, originalUrl }) =>
421+
service.get('/test/:key/projects/:projectId/forms/:id/draft/formList', anonymousEndpoint.openRosa(({ Forms, FormAttachments, env }, { params, originalUrl }) =>
422422
Forms.getByProjectAndXmlFormId(params.projectId, params.id, false, Form.DraftVersion)
423423
.then(getOrNotFound)
424424
.then(ensureDef)
@@ -433,7 +433,7 @@ module.exports = (service, endpoint) => {
433433
draft: true, forms: [ form.withAux('openRosa', { hasAttachments }) ], basePath: path.resolve(originalUrl, '../../../..'), domain: env.domain
434434
})))));
435435

436-
service.get('/test/:key/projects/:projectId/forms/:id/draft/manifest', endpoint.openRosa(({ FormAttachments, Forms, env }, { params, originalUrl }) =>
436+
service.get('/test/:key/projects/:projectId/forms/:id/draft/manifest', anonymousEndpoint.openRosa(({ FormAttachments, Forms, env }, { params, originalUrl }) =>
437437
Forms.getByProjectAndXmlFormId(params.projectId, params.id, false, Form.DraftVersion)
438438
.then(getOrNotFound)
439439
.then(ensureDef)
@@ -442,14 +442,14 @@ module.exports = (service, endpoint) => {
442442
.then((attachments) =>
443443
formManifest({ attachments, basePath: path.resolve(originalUrl, '..'), domain: env.domain })))));
444444

445-
service.get('/test/:key/projects/:projectId/forms/:id/draft.xml', endpoint(({ Forms }, { params }) =>
445+
service.get('/test/:key/projects/:projectId/forms/:id/draft.xml', anonymousEndpoint(({ Forms }, { params }) =>
446446
Forms.getByProjectAndXmlFormId(params.projectId, params.id, true, Form.DraftVersion)
447447
.then(getOrNotFound)
448448
.then(ensureDef)
449449
.then(checkFormToken(params.key))
450450
.then((form) => xml(form.xml))));
451451

452-
service.get('/test/:key/projects/:projectId/forms/:id/draft/attachments/:name', endpoint((container, { params }, request, response) => {
452+
service.get('/test/:key/projects/:projectId/forms/:id/draft/attachments/:name', anonymousEndpoint((container, { params }, request, response) => {
453453
const { FormAttachments, Forms } = container;
454454
return Forms.getByProjectAndXmlFormId(params.projectId, params.id, false, Form.DraftVersion)
455455
.then(getOrNotFound)

lib/resources/submissions.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@ const findMultipart = (files) => {
6565
};
6666
const forceAuthFailed = Problem.translate(Problem.user.insufficientRights, Problem.user.authenticationFailed);
6767

68-
module.exports = (service, endpoint) => {
68+
module.exports = (service, endpoint, anonymousEndpoint) => {
6969

7070
////////////////////////////////////////////////////////////////////////////////
7171
// SUBMISSIONS (OPENROSA)
7272

7373
const openRosaSubmission = (path, draft, getForm) => {
74+
const endpointByTestPath = path.startsWith('/test/:key') ? anonymousEndpoint : endpoint;
75+
7476
// Nonstandard REST; OpenRosa-specific API.
7577
// This bit of silliness is to address the fact that the OpenRosa standards
7678
// specification requires the presence of a HEAD /submission endpoint, but
@@ -81,9 +83,10 @@ module.exports = (service, endpoint) => {
8183
// there is no body content, and the HTTP spec requires that HEAD should
8284
// return exactly what GET would, just without a response body, the only thing
8385
// we can possibly respond with in either case is no body and a 204 code.
84-
service.get(path, endpoint.openRosa(({ Projects }, { params }) =>
86+
service.get(path, endpointByTestPath.openRosa(({ Projects, Forms }, { params, auth }) =>
8587
Projects.getById(params.projectId)
8688
.then(getOrNotFound)
89+
.then(() => (params.xmlFormId ? getForm(auth, params, params.xmlFormId, Forms) : resolve()))
8790
.then(always({ code: 204, body: '' }))));
8891

8992
// Temporary solution to https://github.com/expressjs/multer/issues/1104
@@ -101,7 +104,7 @@ module.exports = (service, endpoint) => {
101104
};
102105

103106
// Nonstandard REST; OpenRosa-specific API.
104-
service.post(path, multipart.any(), multerUtf, multipartErrorHandler, endpoint.openRosa(({ Forms, Submissions, SubmissionAttachments }, { params, files, auth, query, userAgent }) =>
107+
service.post(path, multipart.any(), multerUtf, multipartErrorHandler, endpointByTestPath.openRosa(({ Forms, Submissions, SubmissionAttachments }, { params, files, auth, query, userAgent }) =>
105108
Submission.fromXml(findMultipart(files).buffer)
106109
.then((partial) => getForm(auth, params, partial.xmlFormId, Forms, partial.def.version)
107110
.catch(forceAuthFailed)
@@ -267,7 +270,7 @@ module.exports = (service, endpoint) => {
267270
);
268271

269272
// Create Submission using draftToken
270-
service.post(`/test/:key/projects/:projectId/forms/:formId/draft/submissions`, endpoint(({ Forms, Submissions, SubmissionAttachments }, { params, query, userAgent }, request) =>
273+
service.post(`/test/:key/projects/:projectId/forms/:formId/draft/submissions`, anonymousEndpoint(({ Forms, Submissions, SubmissionAttachments }, { params, query, userAgent }, request) =>
271274
Submission.fromXml(request)
272275
.then((partial) => Forms.getByProjectAndXmlFormId(params.projectId, params.formId, false, Form.DraftVersion)
273276
.then(getOrNotFound)

lib/resources/users.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const { resolve, reject, getOrNotFound } = require('../util/promise');
1717
const { isPresent } = require('../util/util');
1818
const oidc = require('../util/oidc');
1919

20-
module.exports = (service, endpoint) => {
20+
module.exports = (service, endpoint, anonymousEndpoint) => {
2121

2222
// Get a list of user accounts.
2323
// TODO: paging.
@@ -54,8 +54,11 @@ module.exports = (service, endpoint) => {
5454
.then((token) => mail(savedUser.email, 'accountCreated', { token })))
5555
.then(always(savedUser)))));
5656

57+
const endpointByInvalidateQuery = (cb) => (req, ...rest) =>
58+
(req.query.invalidate ? endpoint(cb) : anonymousEndpoint(cb))(req, ...rest);
59+
5760
// TODO/SECURITY: subtle timing attack here.
58-
service.post('/users/reset/initiate', endpoint(({ Users, mail }, { auth, body, query }) =>
61+
service.post('/users/reset/initiate', endpointByInvalidateQuery(({ Users, mail }, { auth, body, query }) =>
5962
(!body.email ? Problem.user.missingParameter({ field: 'email' }) : Users.getByEmail(body.email)
6063
.then((maybeUser) => maybeUser
6164
.map((user) => ((isTrue(query.invalidate))

test/integration/api/app-users.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ const testData = require('../../data/xml');
44

55
describe('api: /projects/:id/app-users', () => {
66
describe('POST', () => {
7-
it('should return 403 unless the user is allowed to create', testService((service) =>
7+
it('should return 403 if unauthenticated', testService((service) =>
88
service.post('/v1/projects/1/app-users')
99
.send({ displayName: 'test1' })
10-
.expect(403)));
10+
.expect(401)));
1111

1212
it('should return the created key', testService((service) =>
1313
service.login('alice', (asAlice) =>
@@ -63,8 +63,8 @@ describe('api: /projects/:id/app-users', () => {
6363
});
6464

6565
describe('GET', () => {
66-
it('should return 403 unless the user is allowed to list', testService((service) =>
67-
service.get('/v1/projects/1/app-users').expect(403)));
66+
it('should return 401 if unauthenticated', testService((service) =>
67+
service.get('/v1/projects/1/app-users').expect(401)));
6868

6969
it('should return a list of tokens in order with merged data', testService((service) =>
7070
service.login('alice', (asAlice) =>
@@ -174,11 +174,11 @@ describe('api: /projects/:id/app-users', () => {
174174
});
175175

176176
describe('/:id DELETE', () => {
177-
it('should return 403 unless the user can delete', testService((service) =>
177+
it('should return 401 if unauthenticated', testService((service) =>
178178
service.login('alice', (asAlice) =>
179179
asAlice.post('/v1/projects/1/app-users').send({ displayName: 'condemned' }).expect(200)
180180
.then(({ body }) =>
181-
service.delete('/v1/projects/1/app-users/' + body.id).expect(403)))));
181+
service.delete('/v1/projects/1/app-users/' + body.id).expect(401)))));
182182

183183
it('should delete the token', testService((service) =>
184184
service.login('alice', (asAlice) =>

0 commit comments

Comments
 (0)