Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rest-logout-session-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Marks the user session as logged out in the Sessions collection when logging out via `POST /v1/logout`. Previously the session cleanup relied on an indirect chain through `watch.users` → `Accounts.onLogout` that could be broken by a race condition, leaving orphaned sessions visible in Device Manager.
5 changes: 2 additions & 3 deletions apps/meteor/server/api/ApiClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { IMethodConnection, IUser } from '@rocket.chat/core-typings';
import type { Route, Router } from '@rocket.chat/http-router';
import { License } from '@rocket.chat/license';
import { Logger } from '@rocket.chat/logger';
import { Users } from '@rocket.chat/models';
import { Sessions, Users } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import type { JoinPathPattern, Method } from '@rocket.chat/rest-typings';
import { ajv } from '@rocket.chat/rest-typings';
Expand Down Expand Up @@ -1135,8 +1135,7 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
},
);

// TODO this can be optmized so places that care about loginTokens being removed are invoked directly
// instead of having to listen to every watch.users event
await Sessions.logoutBySessionIdAndUserId({ loginToken: hashedToken, userId: this.userId });
Comment thread
Rohit3523 marked this conversation as resolved.
void notifyOnUserChangeAsync(async () => {
const userTokens = await Users.findOneById(this.userId, { projection: { [tokenPath]: 1 } });
if (!userTokens) {
Expand Down
51 changes: 51 additions & 0 deletions apps/meteor/tests/end-to-end/api/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5476,6 +5476,57 @@ describe('[Users]', () => {
});
});

describe('[/api/v1/logout]', () => {
let user: TestUser<IUser>;

before(async () => {
user = await createUser();
});

after(() => deleteUser(user));

it('should logout the current user and invalidate the session', async () => {
const userCredentials = await login(user.username, password);

await request.post(api('logout')).set(userCredentials).expect('Content-Type', 'application/json').expect(200);

const meRes = await request.get(api('me')).set(userCredentials);
expect(meRes.statusCode).to.equal(401);
});

(IS_EE ? it : it.skip)('should remove the session from the list after logout', async () => {
Comment thread
Rohit3523 marked this conversation as resolved.
const credentials = await login(user.username, password);
const authToken = credentials['X-Auth-Token'];

await request.post(api('login')).send({ resume: authToken }).expect(200);

const sessionsBefore = await request.get(api('sessions/list')).set(credentials).expect(200);
expect(sessionsBefore.body.sessions).to.have.lengthOf(1);
const sessionIdBefore = sessionsBefore.body.sessions[0]._id;

await request.post(api('logout')).set(credentials).expect(200);

const newCredentials = await login(user.username, password);
const newAuthToken = newCredentials['X-Auth-Token'];

await request.post(api('login')).send({ resume: newAuthToken }).expect(200);

const sessionsAfter = await request.get(api('sessions/list')).set(newCredentials).expect(200);
expect(sessionsAfter.body.sessions).to.have.lengthOf(1);
expect(sessionsAfter.body.sessions[0]._id).to.not.equal(sessionIdBefore);
});

it('should return 401 when not authenticated', async () => {
await request
.post(api('logout'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
});
});
});

describe('[/users.autocomplete]', () => {
after(() => updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user']));

Expand Down
Loading