Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a5ce1c9

Browse files
authored
Add support for redirecting to external pages after logout (#7905)
* Add support for redirecting to external pages after logout This is primarily useful for deployments where the account is managed and needs to be logged out in other places too, like an SSO system. See docs for more information. * Add e2e test and fix Windows instructions * Fix performance gathering stats * use logger
1 parent ac36234 commit a5ce1c9

File tree

9 files changed

+152
-16
lines changed

9 files changed

+152
-16
lines changed

src/@types/global.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
5252
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
5353
import { Skinner } from "../Skinner";
5454
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
55+
import { ConfigOptions } from "../SdkConfig";
5556

5657
/* eslint-disable @typescript-eslint/naming-convention */
5758

@@ -62,6 +63,7 @@ declare global {
6263
Olm: {
6364
init: () => Promise<void>;
6465
};
66+
mxReactSdkConfig: ConfigOptions;
6567

6668
// Needed for Safari, unknown to TypeScript
6769
webkitAudioContext: typeof AudioContext;

src/Lifecycle.ts

+8
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDis
5858
import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog";
5959
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog";
6060
import { setSentryUser } from "./sentry";
61+
import SdkConfig from "./SdkConfig";
6162

6263
const HOMESERVER_URL_KEY = "mx_hs_url";
6364
const ID_SERVER_URL_KEY = "mx_is_url";
@@ -845,6 +846,13 @@ export async function onLoggedOut(): Promise<void> {
845846
stopMatrixClient();
846847
await clearStorage({ deleteEverything: true });
847848
LifecycleCustomisations.onLoggedOutAndStorageCleared?.();
849+
850+
// Do this last so we can make sure all storage has been cleared and all
851+
// customisations got the memo.
852+
if (SdkConfig.get().logout_redirect_url) {
853+
logger.log("Redirecting to external provider to finish logout");
854+
window.location.href = SdkConfig.get().logout_redirect_url;
855+
}
848856
}
849857

850858
/**

src/SdkConfig.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ export interface ISsoRedirectOptions {
2020
on_welcome_page?: boolean; // eslint-disable-line camelcase
2121
}
2222

23+
/* eslint-disable camelcase */
2324
export interface ConfigOptions {
2425
[key: string]: any;
2526

27+
logout_redirect_url?: string;
28+
2629
// sso_immediate_redirect is deprecated in favour of sso_redirect_options.immediate
27-
sso_immediate_redirect?: boolean; // eslint-disable-line camelcase
28-
sso_redirect_options?: ISsoRedirectOptions; // eslint-disable-line camelcase
30+
sso_immediate_redirect?: boolean;
31+
sso_redirect_options?: ISsoRedirectOptions;
2932
}
33+
/* eslint-enable camelcase*/
3034

3135
export const DEFAULTS: ConfigOptions = {
3236
// Brand name of the app
@@ -56,14 +60,14 @@ export default class SdkConfig {
5660
SdkConfig.instance = i;
5761

5862
// For debugging purposes
59-
(<any>window).mxReactSdkConfig = i;
63+
window.mxReactSdkConfig = i;
6064
}
6165

62-
static get() {
66+
public static get() {
6367
return SdkConfig.instance || {};
6468
}
6569

66-
static put(cfg: ConfigOptions) {
70+
public static put(cfg: ConfigOptions) {
6771
const defaultKeys = Object.keys(DEFAULTS);
6872
for (let i = 0; i < defaultKeys.length; ++i) {
6973
if (cfg[defaultKeys[i]] === undefined) {
@@ -73,11 +77,11 @@ export default class SdkConfig {
7377
SdkConfig.setInstance(cfg);
7478
}
7579

76-
static unset() {
80+
public static unset() {
7781
SdkConfig.setInstance({});
7882
}
7983

80-
static add(cfg: ConfigOptions) {
84+
public static add(cfg: ConfigOptions) {
8185
const liveConfig = SdkConfig.get();
8286
const newConfig = Object.assign({}, liveConfig, cfg);
8387
SdkConfig.put(newConfig);

test/end-to-end-tests/Windows.md

+12-6
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,31 @@ and start following these steps to get going:
66
1. Navigate to your working directory (`cd /mnt/c/users/travisr/whatever/matrix-react-sdk` for example).
77
2. Run `sudo apt-get install unzip python3 virtualenv dos2unix`
88
3. Run `dos2unix ./test/end-to-end-tests/*.sh ./test/end-to-end-tests/synapse/*.sh ./test/end-to-end-tests/element/*.sh`
9-
4. Install NodeJS for ubuntu:
9+
4. Install NodeJS for ubuntu:
1010
```bash
11-
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
11+
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
1212
sudo apt-get update
1313
sudo apt-get install nodejs
1414
```
15-
5. Start Element on Windows through `yarn start`
16-
6. While that builds... Run:
15+
5. Run `yarn link` and `yarn install` for all layers from WSL if you haven't already. If you want to switch back to
16+
your Windows host after your tests then you'll need to re-run `yarn install` (and possibly `yarn link`) there too.
17+
Though, do note that you can access `http://localhost:8080` in your Windows-based browser when running webpack in
18+
the WSL environment (it does *not* work the other way around, annoyingly).
19+
6. In WSL, run `yarn start` at the element-web layer to get things going.
20+
7. While that builds... Run:
1721
```bash
1822
sudo apt-get install x11-apps
1923
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
2024
sudo dpkg -i google-chrome-stable_current_amd64.deb
2125
sudo apt -f install
2226
```
23-
7. Run:
27+
8. Get the IP of your host machine out of WSL: `cat /etc/resolv.conf` - use the nameserver IP.
28+
9. Run:
2429
```bash
2530
cd ./test/end-to-end-tests
2631
./synapse/install.sh
2732
./install.sh
28-
./run.sh --app-url http://localhost:8080 --no-sandbox
33+
./run.sh --app-url http://localhost:8080 --log-directory ./logs
2934
```
3035

3136
Note that using `yarn test:e2e` probably won't work for you. You might also have to use the config.json from the
@@ -38,3 +43,4 @@ could probably fix this with enough effort, or you could run a headless Chrome i
3843
Reference material that isn't fully represented in the steps above (but snippets have been borrowed):
3944
* https://virtualizationreview.com/articles/2017/02/08/graphical-programs-on-windows-subsystem-on-linux.aspx
4045
* https://gist.github.com/drexler/d70ab957f964dbef1153d46bd853c775
46+
* https://docs.microsoft.com/en-us/windows/wsl/networking#accessing-windows-networking-apps-from-linux-host-ip

test/end-to-end-tests/src/scenario.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2018 New Vector Ltd
3+
Copyright 2022 The Matrix.org Foundation C.I.C.
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -27,6 +28,7 @@ import { spacesScenarios } from './scenarios/spaces';
2728
import { RestSession } from "./rest/session";
2829
import { stickerScenarios } from './scenarios/sticker';
2930
import { userViewScenarios } from "./scenarios/user-view";
31+
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
3032

3133
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
3234
restCreator: RestSessionCreator): Promise<void> {
@@ -52,7 +54,7 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
5254
console.log("create REST users:");
5355
const charlies = await createRestUsers(restCreator);
5456
await lazyLoadingScenarios(alice, bob, charlies);
55-
// do spaces scenarios last as the rest of the tests may get confused by spaces
57+
// do spaces scenarios last as the rest of the alice/bob tests may get confused by spaces
5658
await spacesScenarios(alice, bob);
5759

5860
// we spawn another session for stickers, partially because it involves injecting
@@ -63,6 +65,12 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
6365
// closing them as we go rather than leaving them all open until the end).
6466
const stickerSession = await createSession("sally");
6567
await stickerScenarios("sally", "ilikestickers", stickerSession, restCreator);
68+
69+
// we spawn yet another session for SSO stuff because it involves authentication and
70+
// logout, which can/does affect other tests dramatically. See notes above regarding
71+
// stickers for the performance loss of doing this.
72+
const ssoSession = await createUser("enterprise_erin");
73+
await ssoCustomisationScenarios(ssoSession);
6674
}
6775

6876
async function createRestUsers(restCreator: RestSessionCreator): Promise<RestMultiSession> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { strict as assert } from "assert";
18+
19+
import { ElementSession } from "../session";
20+
import { logout } from "../usecases/logout";
21+
import { applyConfigChange } from "../util";
22+
23+
export async function ssoCustomisationScenarios(session: ElementSession): Promise<void> {
24+
console.log(" injecting logout customisations for SSO scenarios:");
25+
26+
await session.delay(1000); // wait for dialogs to close
27+
await applyConfigChange(session, {
28+
// we redirect to config.json because it's a predictable page that isn't Element
29+
// itself. We could use example.org, matrix.org, or something else, however this
30+
// puts dependency of external infrastructure on our tests. In the same vein, we
31+
// don't really want to figure out how to ship a `test-landing.html` page when
32+
// running with an uncontrolled Element (via `./run.sh --app-url http://localhost:8080`).
33+
// Using the config.json is just as fine, and we can search for strategic names.
34+
'logout_redirect_url': '/config.json',
35+
});
36+
37+
await logoutCanCauseRedirect(session);
38+
}
39+
40+
async function logoutCanCauseRedirect(session: ElementSession): Promise<void> {
41+
await logout(session, false); // we'll check the login page ourselves, so don't assert
42+
43+
session.log.step("waits for redirect to config.json (as external page)");
44+
const foundLoginUrl = await session.poll(async () => {
45+
const url = session.page.url();
46+
return url === session.url('/config.json');
47+
});
48+
assert(foundLoginUrl);
49+
session.log.done();
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { strict as assert } from 'assert';
18+
19+
import { ElementSession } from "../session";
20+
21+
export async function logout(session: ElementSession, assertLoginPage = true): Promise<void> {
22+
session.log.startGroup("logs out");
23+
24+
session.log.step("navigates to user menu");
25+
const userButton = await session.query('.mx_UserMenu > div.mx_AccessibleButton');
26+
await userButton.click();
27+
session.log.done();
28+
29+
session.log.step("clicks the 'Sign Out' button");
30+
const signOutButton = await session.query('.mx_UserMenu_contextMenu .mx_UserMenu_iconSignOut');
31+
await signOutButton.click();
32+
session.log.done();
33+
34+
if (assertLoginPage) {
35+
const foundLoginUrl = await session.poll(async () => {
36+
const url = session.page.url();
37+
return url === session.url('/#/login');
38+
});
39+
assert(foundLoginUrl);
40+
}
41+
42+
session.log.endGroup();
43+
}

test/end-to-end-tests/src/util.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
Copyright 2018 New Vector Ltd
3-
Copyright 2019 The Matrix.org Foundation C.I.C.
3+
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
44
55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -40,3 +40,14 @@ export const measureStop = function(session: ElementSession, name: string): Prom
4040
window.mxPerformanceMonitor.stop(_name);
4141
}, name);
4242
};
43+
44+
// TODO: Proper types on `config` - for some reason won't accept an import of ConfigOptions.
45+
export async function applyConfigChange(session: ElementSession, config: any): Promise<void> {
46+
await session.page.evaluate((_config) => {
47+
// note: we can't *set* the object because the window version is effectively a pointer.
48+
for (const [k, v] of Object.entries(_config)) {
49+
// @ts-ignore - for some reason it's not picking up on global.d.ts types.
50+
window.mxReactSdkConfig[k] = v;
51+
}
52+
}, config);
53+
}

test/end-to-end-tests/start.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ async function runTests() {
9090
// Collecting all performance monitoring data before closing the session
9191
const measurements = await session.page.evaluate(() => {
9292
let measurements;
93+
94+
// Some tests do redirects away from the app, so don't count those sessions.
95+
if (!window.mxPerformanceMonitor) return JSON.stringify([]);
96+
9397
window.mxPerformanceMonitor.addPerformanceDataCallback({
9498
entryNames: [
9599
window.mxPerformanceEntryNames.REGISTER,
@@ -111,7 +115,7 @@ async function runTests() {
111115
performanceEntries = JSON.parse(measurements);
112116
return session.close();
113117
}));
114-
if (performanceEntries) {
118+
if (performanceEntries?.length > 0) {
115119
fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));
116120
}
117121
if (failure) {

0 commit comments

Comments
 (0)