Merge pull request #850 from vector-im/fix-798

Perform forced logout when access token is invalidated
This commit is contained in:
R Midhun Suresh 2022-08-26 16:31:06 +05:30 committed by GitHub
commit ca4f6d83f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 181 additions and 1 deletions

View File

@ -0,0 +1,81 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {Options as BaseOptions, ViewModel} from "./ViewModel";
import {Client} from "../matrix/Client.js";
import {SegmentType} from "./navigation/index";
type Options = { sessionId: string; } & BaseOptions;
export class ForcedLogoutViewModel extends ViewModel<SegmentType, Options> {
private _sessionId: string;
private _error?: Error;
private _logoutPromise: Promise<void>;
private _showStatus: boolean = false;
private _showSpinner: boolean = false;
constructor(options: Options) {
super(options);
this._sessionId = options.sessionId;
// Start the logout process immediately without any user interaction
this._logoutPromise = this.forceLogout();
}
async forceLogout(): Promise<void> {
try {
const client = new Client(this.platform);
await client.startForcedLogout(this._sessionId);
}
catch (err) {
this._error = err;
// Show the error in the UI
this._showSpinner = false;
this._showStatus = true;
this.emitChange("error");
}
}
async proceed(): Promise<void> {
/**
* The logout should already be completed because we started it from the ctor.
* In case the logout is still proceeding, we will show a message with a spinner.
*/
this._showSpinner = true;
this._showStatus = true;
this.emitChange("showStatus");
await this._logoutPromise;
// At this point, the logout is completed for sure.
if (!this._error) {
this.navigation.push("login", true);
}
}
get status(): string {
if (this._error) {
return this.i18n`Could not log out of device: ${this._error.message}`;
} else {
return this.i18n`Logging out… Please don't close the app.`;
}
}
get showStatus(): boolean {
return this._showStatus;
}
get showSpinner(): boolean {
return this._showSpinner;
}
}

View File

@ -19,6 +19,7 @@ import {SessionViewModel} from "./session/SessionViewModel.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {LoginViewModel} from "./login/LoginViewModel"; import {LoginViewModel} from "./login/LoginViewModel";
import {LogoutViewModel} from "./LogoutViewModel"; import {LogoutViewModel} from "./LogoutViewModel";
import {ForcedLogoutViewModel} from "./ForcedLogoutViewModel";
import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
import {ViewModel} from "./ViewModel"; import {ViewModel} from "./ViewModel";
@ -30,6 +31,7 @@ export class RootViewModel extends ViewModel {
this._sessionLoadViewModel = null; this._sessionLoadViewModel = null;
this._loginViewModel = null; this._loginViewModel = null;
this._logoutViewModel = null; this._logoutViewModel = null;
this._forcedLogoutViewModel = null;
this._sessionViewModel = null; this._sessionViewModel = null;
this._pendingClient = null; this._pendingClient = null;
} }
@ -38,18 +40,24 @@ export class RootViewModel extends ViewModel {
this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation()));
this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation()));
this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation()));
this.track(this.navigation.observe("logout").subscribe(() => this._applyNavigation()));
this._applyNavigation(true); this._applyNavigation(true);
} }
async _applyNavigation(shouldRestoreLastUrl) { async _applyNavigation(shouldRestoreLastUrl) {
const isLogin = this.navigation.path.get("login"); const isLogin = this.navigation.path.get("login");
const logoutSessionId = this.navigation.path.get("logout")?.value; const logoutSessionId = this.navigation.path.get("logout")?.value;
const isForcedLogout = this.navigation.path.get("forced")?.value;
const sessionId = this.navigation.path.get("session")?.value; const sessionId = this.navigation.path.get("session")?.value;
const loginToken = this.navigation.path.get("sso")?.value; const loginToken = this.navigation.path.get("sso")?.value;
if (isLogin) { if (isLogin) {
if (this.activeSection !== "login") { if (this.activeSection !== "login") {
this._showLogin(); this._showLogin();
} }
} else if (logoutSessionId && isForcedLogout) {
if (this.activeSection !== "forced-logout") {
this._showForcedLogout(logoutSessionId);
}
} else if (logoutSessionId) { } else if (logoutSessionId) {
if (this.activeSection !== "logout") { if (this.activeSection !== "logout") {
this._showLogout(logoutSessionId); this._showLogout(logoutSessionId);
@ -136,6 +144,12 @@ export class RootViewModel extends ViewModel {
}); });
} }
_showForcedLogout(sessionId) {
this._setSection(() => {
this._forcedLogoutViewModel = new ForcedLogoutViewModel(this.childOptions({sessionId}));
});
}
_showSession(client) { _showSession(client) {
this._setSection(() => { this._setSection(() => {
this._sessionViewModel = new SessionViewModel(this.childOptions({client})); this._sessionViewModel = new SessionViewModel(this.childOptions({client}));
@ -164,6 +178,8 @@ export class RootViewModel extends ViewModel {
return "login"; return "login";
} else if (this._logoutViewModel) { } else if (this._logoutViewModel) {
return "logout"; return "logout";
} else if (this._forcedLogoutViewModel) {
return "forced-logout";
} else if (this._sessionPickerViewModel) { } else if (this._sessionPickerViewModel) {
return "picker"; return "picker";
} else if (this._sessionLoadViewModel) { } else if (this._sessionLoadViewModel) {
@ -180,6 +196,7 @@ export class RootViewModel extends ViewModel {
this._sessionLoadViewModel = this.disposeTracked(this._sessionLoadViewModel); this._sessionLoadViewModel = this.disposeTracked(this._sessionLoadViewModel);
this._loginViewModel = this.disposeTracked(this._loginViewModel); this._loginViewModel = this.disposeTracked(this._loginViewModel);
this._logoutViewModel = this.disposeTracked(this._logoutViewModel); this._logoutViewModel = this.disposeTracked(this._logoutViewModel);
this._forcedLogoutViewModel = this.disposeTracked(this._forcedLogoutViewModel);
this._sessionViewModel = this.disposeTracked(this._sessionViewModel); this._sessionViewModel = this.disposeTracked(this._sessionViewModel);
// now set it again // now set it again
setter(); setter();
@ -187,6 +204,7 @@ export class RootViewModel extends ViewModel {
this._sessionLoadViewModel && this.track(this._sessionLoadViewModel); this._sessionLoadViewModel && this.track(this._sessionLoadViewModel);
this._loginViewModel && this.track(this._loginViewModel); this._loginViewModel && this.track(this._loginViewModel);
this._logoutViewModel && this.track(this._logoutViewModel); this._logoutViewModel && this.track(this._logoutViewModel);
this._forcedLogoutViewModel && this.track(this._forcedLogoutViewModel);
this._sessionViewModel && this.track(this._sessionViewModel); this._sessionViewModel && this.track(this._sessionViewModel);
this.emitChange("activeSection"); this.emitChange("activeSection");
} }
@ -195,6 +213,7 @@ export class RootViewModel extends ViewModel {
get sessionViewModel() { return this._sessionViewModel; } get sessionViewModel() { return this._sessionViewModel; }
get loginViewModel() { return this._loginViewModel; } get loginViewModel() { return this._loginViewModel; }
get logoutViewModel() { return this._logoutViewModel; } get logoutViewModel() { return this._logoutViewModel; }
get forcedLogoutViewModel() { return this._forcedLogoutViewModel; }
get sessionPickerViewModel() { return this._sessionPickerViewModel; } get sessionPickerViewModel() { return this._sessionPickerViewModel; }
get sessionLoadViewModel() { return this._sessionLoadViewModel; } get sessionLoadViewModel() { return this._sessionLoadViewModel; }
} }

View File

@ -23,6 +23,7 @@ export type SegmentType = {
"session": string | boolean; "session": string | boolean;
"sso": string; "sso": string;
"logout": true; "logout": true;
"forced": true;
"room": string; "room": string;
"rooms": string[]; "rooms": string[];
"settings": true; "settings": true;
@ -58,6 +59,8 @@ function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<Se
return type === "lightbox" || type === "right-panel"; return type === "lightbox" || type === "right-panel";
case "right-panel": case "right-panel":
return type === "details"|| type === "members" || type === "member"; return type === "details"|| type === "members" || type === "member";
case "logout":
return type === "forced";
default: default:
return false; return false;
} }

View File

@ -28,6 +28,7 @@ import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
import {ViewModel} from "../ViewModel"; import {ViewModel} from "../ViewModel";
import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";
import {SyncStatus} from "../../matrix/Sync.js";
export class SessionViewModel extends ViewModel { export class SessionViewModel extends ViewModel {
constructor(options) { constructor(options) {
@ -45,6 +46,7 @@ export class SessionViewModel extends ViewModel {
this._gridViewModel = null; this._gridViewModel = null;
this._createRoomViewModel = null; this._createRoomViewModel = null;
this._setupNavigation(); this._setupNavigation();
this._setupForcedLogoutOnAccessTokenInvalidation();
} }
_setupNavigation() { _setupNavigation() {
@ -93,6 +95,23 @@ export class SessionViewModel extends ViewModel {
this._updateRightPanel(); this._updateRightPanel();
} }
_setupForcedLogoutOnAccessTokenInvalidation() {
this.track(this._client.sync.status.subscribe(status => {
if (status === SyncStatus.Stopped) {
const error = this._client.sync.error;
if (error?.errcode === "M_UNKNOWN_TOKEN") {
// Access token is no longer valid, so force the user to log out
const segments = [
this.navigation.segment("logout", this.id),
this.navigation.segment("forced", true),
];
const path = this.navigation.pathFrom(segments);
this.navigation.applyPath(path);
}
}
}));
}
get id() { get id() {
return this._client.sessionId; return this._client.sessionId;
} }

View File

@ -421,6 +421,14 @@ export class Client {
}); });
} }
startForcedLogout(sessionId) {
return this._platform.logger.run("forced-logout", async log => {
this._sessionId = sessionId;
log.set("id", this._sessionId);
await this.deleteSession(log);
});
}
dispose() { dispose() {
if (this._reconnectSubscription) { if (this._reconnectSubscription) {
this._reconnectSubscription(); this._reconnectSubscription();

View File

@ -0,0 +1,47 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {TemplateView} from "./general/TemplateView";
import {spinner} from "./common.js";
export class ForcedLogoutView extends TemplateView {
render(t) {
return t.div({ className: "LogoutScreen" }, [
t.div({ className: "content" },
t.map(vm => vm.showStatus, (showStatus, t, vm) => {
if (showStatus) {
return t.p({ className: "status" }, [
spinner(t, { hidden: vm => !vm.showSpinner }),
t.span(vm => vm.status)
]);
}
else {
return t.div([
t.p("Your access token is no longer valid! You can reauthenticate in the next screen."),
t.div({ className: "button-row" }, [
t.button({
className: "button-action primary",
type: "submit",
onClick: () => vm.proceed(),
}, vm.i18n`Proceed`)
]),
]);
}
})
),
]);
}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
import {SessionView} from "./session/SessionView.js"; import {SessionView} from "./session/SessionView.js";
import {LoginView} from "./login/LoginView"; import {LoginView} from "./login/LoginView";
import {LogoutView} from "./LogoutView.js"; import {LogoutView} from "./LogoutView.js";
import {ForcedLogoutView} from "./ForcedLogoutView.js";
import {SessionLoadView} from "./login/SessionLoadView.js"; import {SessionLoadView} from "./login/SessionLoadView.js";
import {SessionPickerView} from "./login/SessionPickerView.js"; import {SessionPickerView} from "./login/SessionPickerView.js";
import {TemplateView} from "./general/TemplateView"; import {TemplateView} from "./general/TemplateView";
@ -39,6 +40,8 @@ export class RootView extends TemplateView {
return new LoginView(vm.loginViewModel); return new LoginView(vm.loginViewModel);
case "logout": case "logout":
return new LogoutView(vm.logoutViewModel); return new LogoutView(vm.logoutViewModel);
case "forced-logout":
return new ForcedLogoutView(vm.forcedLogoutViewModel);
case "picker": case "picker":
return new SessionPickerView(vm.sessionPickerViewModel); return new SessionPickerView(vm.sessionPickerViewModel);
case "redirecting": case "redirecting":