Skip to content

Commit

Permalink
Add endpoint to delete current session
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-white committed Jul 12, 2023
1 parent 52bb6d2 commit 9c5b28e
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 13 deletions.
32 changes: 19 additions & 13 deletions lib/resources/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,30 @@ module.exports = (service, endpoint) => {
service.get('/sessions/restore', endpoint((_, { auth }) =>
auth.session.orElse(Problem.user.notFound())));

const logOut = (Sessions, auth, session) =>
auth.canOrReject('session.end', session.actor)
.then(() => Sessions.terminate(session))
.then(() => (_, response) => {
// revoke the cookie associated w the session, if the session was used to
// terminate itself.
// TODO: repetitive w above.
if (session.token === auth.session.map((s) => s.token).orNull())
response.cookie('__Host-session', 'null', { path: '/', expires: new Date(0),
httpOnly: true, secure: true, sameSite: 'strict' });

return success;
});

service.delete('/sessions/current', endpoint(({ Sessions }, { auth }) =>
auth.session.map(session => logOut(Sessions, auth, session))
.orElse(Problem.user.notFound())));

// here we always throw a 403 even if the token doesn't exist to prevent
// information leakage.
// TODO: but a timing attack still exists here. :(
service.delete('/sessions/:token', endpoint(({ Sessions }, { auth, params }) =>
Sessions.getByBearerToken(params.token)
.then(getOrReject(Problem.user.insufficientRights()))
.then((session) => auth.canOrReject('session.end', session.actor)
.then(() => Sessions.terminate(session))
.then(() => (_, response) => {
// revoke the cookie associated w the session, if the session was used to
// terminate itself.
// TODO: repetitive w above.
if (session.token === auth.session.map((s) => s.token).orNull())
response.cookie('__Host-session', 'null', { path: '/', expires: new Date(0),
httpOnly: true, secure: true, sameSite: 'strict' });

return success;
}))));

.then(session => logOut(Sessions, auth, session))));
};

29 changes: 29 additions & 0 deletions test/integration/api/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,35 @@ describe('api: /sessions', () => {
})))));
});

describe('/current DELETE', () => {
it('should return a 404 if no token was specified', testService(service =>
service.delete('/v1/sessions/current')
.expect(404)));

it('should invalidate the token if successful', testService(async (service) => {
const { body: session } = await service.post('/v1/sessions')
.send({ email: 'alice@getodk.org', password: 'alice' })
.expect(200);
const { token } = session;
const { body } = await service.delete('/v1/sessions/current')
.set('Authorization', `Bearer ${token}`)
.expect(200);
body.should.eql({ success: true });
await service.get('/v1/users/current')
.set('Authorization', `Bearer ${token}`)
.expect(401);
}));

it('should not allow app users to delete their own sessions', testService(async (service) => {
const asBob = await service.login('bob');
const { body: appUser } = await asBob.post('/v1/projects/1/app-users')
.send({ displayName: 'test app user' })
.expect(200);
await service.delete(`/v1/key/${appUser.token}/sessions/current`)
.expect(403);
}));
});

// this isn't exactly the right place for this but i just want to check the
// whole stack in addition to the unit tests.
describe('cookie CSRF auth', () => {
Expand Down

0 comments on commit 9c5b28e

Please sign in to comment.