-
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?
Return 401 if authentication is not provided #1488
Conversation
3f2926d
to
82217e7
Compare
@@ -499,7 +507,7 @@ describe('/projects/:id/assignments', () => { | |||
it('should assign the actor by role numeric id', testService((service) => | |||
service.login('bob', (asBob) => | |||
Promise.all([ | |||
service.get('/v1/roles/manager').expect(200).then(({ body }) => body.id), | |||
asBob.get('/v1/roles/manager').expect(200).then(({ body }) => body.id), |
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.
This endpoint does not currently require authentication. How does that expectation changing relate to 401 vs 403 errors?
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.
Those two changes (more 401 errors + requiring auth for /v1/roles) both touch on the theme of anonymity. However, I agree that they're also conceptually distinct. If it's easy to pull the two changes apart, that'd make this PR a little smaller, which sounds nice. I'm also OK with reviewing both changes as part of this PR. I like the idea of having the /v1/roles endpoints check auth.
@@ -128,9 +125,6 @@ const authHandler = ({ Sessions, Users, Auth }, context) => { | |||
|
|||
// if POST run authentication as usual but we'll have to check CSRF afterwards. | |||
return maybeSession.then((cxt) => { // we have to use cxt rather than context for the linter | |||
// if authentication failed anyway, just do nothing. | |||
if ((cxt == null) || cxt.auth.session.isEmpty()) return; |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
const preprocessors = [ queryOptionsHandler, userAgentHandler ]; | |
const commonPreprocessors = [ queryOptionsHandler, userAgentHandler ]; |
|
||
//////////////////////////////////////////////////////////////////////////////// | ||
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Would this code be more intuitive if the endpoint
implementation was passed in rather than being derived from path
?
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 comment
The 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). 👍
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 comment
The reason will be displayed to describe this comment to others. Learn more.
It's not obvious how this change is related to 401
responses; any pointers?
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Similar question from me. I'd expect every endpoint that calls auth.canOrReject()
to have a test asserting a 403. This test now never reaches the actual endpoint logic: it's just being stopped by the preprocessor, which we test elsewhere.
From https://www.rfc-editor.org/rfc/rfc9110#status.401:
Is this relevant, and if so is it covered by this PR? |
Checking one endpoint at random, it looks like some API docs might need updates: Lines 1965 to 1970 in 889d208
|
I don't think we ever send that header, so I don't think this PR needs to do so. How about we continue discussing that header in #851? |
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.
For the GET /users endpoint, could you remove the auth.isAuthenticated
check? Since the endpoint uses endpoint
, we can be sure that it's authenticated.
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.
Or maybe it's a nice safeguard? It sort of feels redundant to me now.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
endpointByInvalidateQuery
and endpointByTestPath
above make me think that if/when we update endpoint
to accept an options object, we should make the anonymous
option take a callback.
Maybe it'd be reasonable to do something like that now? What if anonymousEndpoint
accepted a function returning true
/false
, and then anonymousEndpoint
either called authHandler
or not based on the result? We could even make the function required, since Ramda F
would be easy to pass in.
Just a thought, I'm also happy with the approach here. 👍
service.login('alice', (asAlice) => | ||
service.login('bob', (asBob) => |
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.
Doesn't need to change, but just FYI, you can pass an array to service.login()
:
service.login(['alice', 'bob'], (asAlice, asBob) =>
@@ -181,7 +181,7 @@ describe('api: /sessions', () => { | |||
describe('/:token DELETE', () => { | |||
it('should return a 403 if the token does not exist', testService((service) => | |||
service.delete('/v1/sessions/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') | |||
.expect(403))); | |||
.expect(401))); |
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 feel like this test is looking at nonexistence, not lack of authentication. I think the test should have Alice send the request, then assert that it's a 403. This endpoint is unusual in that it returns a 403 in the nonexistence case instead of a 404.
@@ -162,7 +162,7 @@ describe('api: /sessions', () => { | |||
service.get('/v1/sessions/restore') | |||
.set('X-Forwarded-Proto', 'https') | |||
.set('Cookie', 'session: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') |
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.
This cookie looks unusual, shouldn't it be =
instead of :
?
@@ -162,7 +162,7 @@ describe('api: /sessions', () => { | |||
service.get('/v1/sessions/restore') | |||
.set('X-Forwarded-Proto', 'https') | |||
.set('Cookie', 'session: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') | |||
.expect(404))); | |||
.expect(401))); |
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.
This is probably on your radar already, but the change from 404 to 401 will require a change to restoreSession()
in Frontend.
@@ -301,7 +289,7 @@ describe('preprocessors', () => { | |||
) | |||
)).should.be.rejectedWith(Problem, { problemCode: 401.2 })); | |||
|
|||
it('should do nothing on cookie auth with incorrect session token for non-GET requests', () => | |||
it('should reject cookie auth with incorrect session token for non-GET requests', () => |
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.
Maybe we can remove the part of the test description about "for non-GET requests". We will now fail even GET requests.
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.
The approach that's being taken here seems like a good one to me. I left some questions/thoughts above, but I think this will be a nice change for Central.
Prerequisite for getodk/central#1001
What has been done to verify that this works as intended?
All tests are passing.
Some tests are updated to reflect the correct requirement.
Why is this the best possible solution? Were any other approaches considered?
It is necessary to distinguish between 401 and 403, previously we were relying on resources to perform authorization even when authentication failed.
How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
This PR shouldn't be merged until corresponding frontend changes are ready. There are some places where frontend code expects 403, that needs to be updated.
Does this change require updates to the API documentation? If so, please update docs/api.yaml as part of this PR.
No
Before submitting this PR, please make sure you have:
make test
and confirmed all checks still pass OR confirm CircleCI build passes