diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 3e51b45ee0..719b0a45fe 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -204,6 +204,7 @@ export async function attemptDelegatedAuthLogin( fragmentAfterLogin?: string, ): Promise { if (queryParams.code && queryParams.state) { + console.log("We have OIDC params - attempting OIDC login"); return attemptOidcNativeLogin(queryParams); } @@ -297,6 +298,8 @@ export function attemptTokenLogin( return Promise.resolve(false); } + console.log("We have token login params - attempting token login"); + const homeserver = localStorage.getItem(SSO_HOMESERVER_URL_KEY); const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY) ?? undefined; if (!homeserver) { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 1da54addf4..49981b781a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -293,14 +293,6 @@ export default class MatrixChat extends React.PureComponent { RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); - // Force users to go through the soft logout page if they're soft logged out - if (Lifecycle.isSoftLogout()) { - // When the session loads it'll be detected as soft logged out and a dispatch - // will be sent out to say that, triggering this MatrixChat to show the soft - // logout page. - Lifecycle.loadSession(); - } - this.dispatcherRef = dis.register(this.onAction); this.themeWatcher = new ThemeWatcher(); @@ -314,52 +306,77 @@ export default class MatrixChat extends React.PureComponent { // we don't do it as react state as i'm scared about triggering needless react refreshes. this.subTitleStatus = ""; - // the first thing to do is to try the token params in the query-string - // if the session isn't soft logged out (ie: is a clean session being logged in) - if (!Lifecycle.isSoftLogout()) { - Lifecycle.attemptDelegatedAuthLogin( - this.props.realQueryParams, - this.props.defaultDeviceDisplayName, - this.getFragmentAfterLogin(), - ).then(async (loggedIn): Promise => { - if ( - this.props.realQueryParams?.loginToken || - this.props.realQueryParams?.code || - this.props.realQueryParams?.state - ) { - // remove the loginToken or auth code from the URL regardless - this.props.onTokenLoginCompleted(); - } + initSentry(SdkConfig.get("sentry")); - if (loggedIn) { - this.tokenLogin = true; + this.initSession().catch((err) => { + // TODO: show an error screen, rather than a spinner of doom + logger.error("Error initialising Matrix session", err); + }); + } - // Create and start the client - // accesses the new credentials just set in storage during attemptTokenLogin - // and sets logged in state - await Lifecycle.restoreFromLocalStorage({ - ignoreGuest: true, - }); - return this.postLoginSetup(); - } - - // if the user has followed a login or register link, don't reanimate - // the old creds, but rather go straight to the relevant page - const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null; - const restoreSuccess = await this.loadSession(); - if (restoreSuccess) { - return true; - } - - if (firstScreen === "login" || firstScreen === "register" || firstScreen === "forgot_password") { - this.showScreenAfterLogin(); - } - - return false; - }); + /** + * Do what we can to establish a Matrix session. + * + * * Special-case soft-logged-out sessions + * * If we have OIDC or token login parameters, follow them + * * If we have a guest access token in the query params, use that + * * If we have parameters in local storage, use them + * * Attempt to auto-register as a guest + * * If all else fails, present a login screen. + */ + private async initSession(): Promise { + // If the user was soft-logged-out, we want to make the SoftLogout component responsible for doing any + // token auth (rather than Lifecycle.attemptDelegatedAuthLogin), since SoftLogout knows about submitting the + // device ID and preserving the session. + // + // So, we start by special-casing soft-logged-out sessions. + if (Lifecycle.isSoftLogout()) { + // When the session loads it'll be detected as soft logged out and a dispatch + // will be sent out to say that, triggering this MatrixChat to show the soft + // logout page. + Lifecycle.loadSession(); + return; } - initSentry(SdkConfig.get("sentry")); + // Otherwise, the first thing to do is to try the token params in the query-string + const delegatedAuthSucceeded = await Lifecycle.attemptDelegatedAuthLogin( + this.props.realQueryParams, + this.props.defaultDeviceDisplayName, + this.getFragmentAfterLogin(), + ); + + // remove the loginToken or auth code from the URL regardless + if ( + this.props.realQueryParams?.loginToken || + this.props.realQueryParams?.code || + this.props.realQueryParams?.state + ) { + this.props.onTokenLoginCompleted(); + } + + if (delegatedAuthSucceeded) { + // token auth/OIDC worked! Time to fire up the client. + this.tokenLogin = true; + + // Create and start the client + // accesses the new credentials just set in storage during attemptDelegatedAuthLogin + // and sets logged in state + await Lifecycle.restoreFromLocalStorage({ ignoreGuest: true }); + await this.postLoginSetup(); + return; + } + + // if the user has followed a login or register link, don't reanimate + // the old creds, but rather go straight to the relevant page + const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null; + const restoreSuccess = await this.loadSession(); + if (restoreSuccess) { + return; + } + + if (firstScreen === "login" || firstScreen === "register" || firstScreen === "forgot_password") { + this.showScreenAfterLogin(); + } } private async postLoginSetup(): Promise { diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 06fc543c45..0dc2fc569d 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -520,6 +520,36 @@ describe("", () => { }); }); + describe("with a soft-logged-out session", () => { + const mockidb: Record> = {}; + const mockLocalStorage: Record = { + mx_hs_url: serverConfig.hsUrl, + mx_is_url: serverConfig.isUrl, + mx_access_token: accessToken, + mx_user_id: userId, + mx_device_id: deviceId, + mx_soft_logout: "true", + }; + + beforeEach(() => { + localStorageGetSpy.mockImplementation((key: unknown) => mockLocalStorage[key as string] || ""); + + mockClient.loginFlows.mockResolvedValue({ flows: [{ type: "m.login.password" }] }); + + jest.spyOn(StorageManager, "idbLoad").mockImplementation(async (table, key) => { + const safeKey = Array.isArray(key) ? key[0] : key; + return mockidb[table]?.[safeKey]; + }); + }); + + it("should show the soft-logout page", async () => { + const result = getComponent(); + + await result.findByText("You're signed out"); + expect(result.container).toMatchSnapshot(); + }); + }); + describe("login via key/pass", () => { let loginClient!: ReturnType; diff --git a/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 9acb721a82..5bd7b22f4e 100644 --- a/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/test/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -20,6 +20,129 @@ exports[` should render spinner while app is loading 1`] = ` `; +exports[` with a soft-logged-out session should show the soft-logout page 1`] = ` +
+
+
+
+ +
+ +
+
+
+

+ You're signed out +

+

+ Sign in +

+
+
+

+ Regain access to your account and recover encryption keys stored in this session. Without them, you won't be able to read all of your secure messages in any session. +

+
+ + +
+
+ Sign In +
+ +
+
+

+ Clear personal data +

+

+ Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account. +

+
+
+ Clear all data +
+
+
+
+ +
+
+`; + exports[` with an existing session onAction() room actions leave_room for a room should launch a confirmation modal 1`] = `