From 3135db0e7258a22f6e0ddf310e6498862ccd8454 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Oliver=20J=C3=A4gle?= <mrsimpson@users.noreply.github.com>
Date: Sat, 21 Nov 2020 03:18:45 +0100
Subject: [PATCH] [NEW] REST endpoint to log user out from other clients #19246

---
 app/api/server/v1/users.js       | 10 ++++
 tests/end-to-end/api/01-users.js | 88 ++++++++++++++++++++++++++++++++
 2 files changed, 98 insertions(+)

diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js
index b182c363305..47338e67d2e 100644
--- a/app/api/server/v1/users.js
+++ b/app/api/server/v1/users.js
@@ -779,6 +779,16 @@ API.v1.addRoute('users.requestDataDownload', { authRequired: true }, {
 	},
 });
 
+API.v1.addRoute('users.logoutOtherClients', { authRequired: true }, {
+	post() {
+		try {
+			Meteor.runAsUser(this.userId, () => API.v1.success(Meteor.call('logoutOtherClients')));
+		} catch (error) {
+			return API.v1.failure(error);
+		}
+	},
+});
+
 API.v1.addRoute('users.autocomplete', { authRequired: true }, {
 	get() {
 		const { selector } = this.queryParams;
diff --git a/tests/end-to-end/api/01-users.js b/tests/end-to-end/api/01-users.js
index 9607852e2a9..2f19bd900ea 100644
--- a/tests/end-to-end/api/01-users.js
+++ b/tests/end-to-end/api/01-users.js
@@ -20,6 +20,47 @@ import { updatePermission, updateSetting } from '../../data/permissions.helper';
 import { createUser, login, deleteUser, getUserStatus } from '../../data/users.helper.js';
 import { createRoom } from '../../data/rooms.helper';
 
+function createTestUser() {
+	return new Promise((resolve) => {
+		const username = `user.test.${ Date.now() }`;
+		const email = `${ username }@rocket.chat`;
+		request.post(api('users.create'))
+			.set(credentials)
+			.send({ email, name: username, username, password })
+			.end((err, res) => resolve(res.body.user));
+	});
+}
+
+function loginTestUser(user) {
+	return new Promise((resolve, reject) => {
+		request.post(api('login'))
+			.send({
+				user: user.username,
+				password,
+			})
+			.expect('Content-Type', 'application/json')
+			.expect(200)
+			.expect((res) => {
+				const userCredentials = {};
+				userCredentials['X-Auth-Token'] = res.body.data.authToken;
+				userCredentials['X-User-Id'] = res.body.data.userId;
+				resolve(userCredentials);
+			})
+			.end((err) => (err ? reject(err) : resolve()));
+	});
+}
+
+function deleteTestUser(user) {
+	return new Promise((resolve) => {
+		request.post(api('users.delete'))
+			.set(credentials)
+			.send({
+				userId: user._id,
+			})
+			.end(resolve);
+	});
+}
+
 describe('[Users]', function() {
 	this.retries(0);
 
@@ -2288,6 +2329,53 @@ describe('[Users]', function() {
 		});
 	});
 
+	describe('[/users.logoutOtherClients]', function() {
+		let user;
+		let userCredentials;
+		let newCredentials;
+
+		this.timeout(20000);
+
+		before(async () => {
+			user = await createTestUser();
+			userCredentials = await loginTestUser(user);
+			newCredentials = await loginTestUser(user);
+		});
+		after(async () => {
+			await deleteTestUser(user);
+			user = undefined;
+		});
+
+		it('should invalidate all active sesions', (done) => {
+			/* We want to validate that the login with the "old" credentials fails
+			However, the removal of the tokens is done asynchronously.
+			Thus, we check that within the next seconds, at least one try to
+			access an authentication requiring route fails */
+			let counter = 0;
+
+			async function checkAuthenticationFails() {
+				const result = await request.get(api('me'))
+					.set(userCredentials);
+				return result.statusCode === 401;
+			}
+
+			async function tryAuthentication() {
+				if (await checkAuthenticationFails()) {
+					done();
+				} else if (++counter < 20) {
+					setTimeout(tryAuthentication, 1000);
+				} else {
+					done('Session did not invalidate in time');
+				}
+			}
+
+			request.post(api('users.logoutOtherClients'))
+				.set(newCredentials)
+				.expect(200)
+				.then(tryAuthentication);
+		});
+	});
+
 	describe('[/users.autocomplete]', () => {
 		it('should return an empty list when the user does not have the necessary permission', (done) => {
 			updatePermission('view-outside-room', []).then(() => {
-- 
GitLab