Clean up the MatrixChat initSession code (#11403)

`async` functions are a thing, and they make this much more comprehensible.
This commit is contained in:
Richard van der Hoff 2023-08-14 13:52:08 +01:00 committed by GitHub
parent f65c6726c9
commit 672ad98ec7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 223 additions and 50 deletions

View file

@ -204,6 +204,7 @@ export async function attemptDelegatedAuthLogin(
fragmentAfterLogin?: string, fragmentAfterLogin?: string,
): Promise<boolean> { ): Promise<boolean> {
if (queryParams.code && queryParams.state) { if (queryParams.code && queryParams.state) {
console.log("We have OIDC params - attempting OIDC login");
return attemptOidcNativeLogin(queryParams); return attemptOidcNativeLogin(queryParams);
} }
@ -297,6 +298,8 @@ export function attemptTokenLogin(
return Promise.resolve(false); return Promise.resolve(false);
} }
console.log("We have token login params - attempting token login");
const homeserver = localStorage.getItem(SSO_HOMESERVER_URL_KEY); const homeserver = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY) ?? undefined; const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY) ?? undefined;
if (!homeserver) { if (!homeserver) {

View file

@ -293,14 +293,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); 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.dispatcherRef = dis.register(this.onAction);
this.themeWatcher = new ThemeWatcher(); this.themeWatcher = new ThemeWatcher();
@ -314,33 +306,64 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// we don't do it as react state as i'm scared about triggering needless react refreshes. // we don't do it as react state as i'm scared about triggering needless react refreshes.
this.subTitleStatus = ""; this.subTitleStatus = "";
// the first thing to do is to try the token params in the query-string initSentry(SdkConfig.get("sentry"));
// if the session isn't soft logged out (ie: is a clean session being logged in)
if (!Lifecycle.isSoftLogout()) { this.initSession().catch((err) => {
Lifecycle.attemptDelegatedAuthLogin( // TODO: show an error screen, rather than a spinner of doom
logger.error("Error initialising Matrix session", err);
});
}
/**
* 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<void> {
// 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;
}
// 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.realQueryParams,
this.props.defaultDeviceDisplayName, this.props.defaultDeviceDisplayName,
this.getFragmentAfterLogin(), this.getFragmentAfterLogin(),
).then(async (loggedIn): Promise<boolean | void> => { );
// remove the loginToken or auth code from the URL regardless
if ( if (
this.props.realQueryParams?.loginToken || this.props.realQueryParams?.loginToken ||
this.props.realQueryParams?.code || this.props.realQueryParams?.code ||
this.props.realQueryParams?.state this.props.realQueryParams?.state
) { ) {
// remove the loginToken or auth code from the URL regardless
this.props.onTokenLoginCompleted(); this.props.onTokenLoginCompleted();
} }
if (loggedIn) { if (delegatedAuthSucceeded) {
// token auth/OIDC worked! Time to fire up the client.
this.tokenLogin = true; this.tokenLogin = true;
// Create and start the client // Create and start the client
// accesses the new credentials just set in storage during attemptTokenLogin // accesses the new credentials just set in storage during attemptDelegatedAuthLogin
// and sets logged in state // and sets logged in state
await Lifecycle.restoreFromLocalStorage({ await Lifecycle.restoreFromLocalStorage({ ignoreGuest: true });
ignoreGuest: true, await this.postLoginSetup();
}); return;
return this.postLoginSetup();
} }
// if the user has followed a login or register link, don't reanimate // if the user has followed a login or register link, don't reanimate
@ -348,18 +371,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null; const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null;
const restoreSuccess = await this.loadSession(); const restoreSuccess = await this.loadSession();
if (restoreSuccess) { if (restoreSuccess) {
return true; return;
} }
if (firstScreen === "login" || firstScreen === "register" || firstScreen === "forgot_password") { if (firstScreen === "login" || firstScreen === "register" || firstScreen === "forgot_password") {
this.showScreenAfterLogin(); this.showScreenAfterLogin();
} }
return false;
});
}
initSentry(SdkConfig.get("sentry"));
} }
private async postLoginSetup(): Promise<void> { private async postLoginSetup(): Promise<void> {

View file

@ -520,6 +520,36 @@ describe("<MatrixChat />", () => {
}); });
}); });
describe("with a soft-logged-out session", () => {
const mockidb: Record<string, Record<string, string>> = {};
const mockLocalStorage: Record<string, string> = {
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", () => { describe("login via key/pass", () => {
let loginClient!: ReturnType<typeof getMockClientWithEventEmitter>; let loginClient!: ReturnType<typeof getMockClientWithEventEmitter>;

View file

@ -20,6 +20,129 @@ exports[`<MatrixChat /> should render spinner while app is loading 1`] = `
</div> </div>
`; `;
exports[`<MatrixChat /> with a soft-logged-out session should show the soft-logout page 1`] = `
<div>
<div
class="mx_AuthPage"
>
<div
class="mx_AuthPage_modal"
>
<div
class="mx_AuthHeader"
>
<aside
class="mx_AuthHeaderLogo"
>
Matrix
</aside>
<div
class="mx_Dropdown mx_AuthBody_language"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</div>
<main
class="mx_AuthBody"
>
<h1>
You're signed out
</h1>
<h2>
Sign in
</h2>
<div>
<form>
<p>
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.
</p>
<div
class="mx_Field mx_Field_input"
>
<input
id="mx_Field_1"
label="Password"
placeholder="Password"
type="password"
value=""
/>
<label
for="mx_Field_1"
>
Password
</label>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Sign In
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
Forgotten your password?
</div>
</form>
</div>
<h2>
Clear personal data
</h2>
<p>
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.
</p>
<div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
role="button"
tabindex="0"
>
Clear all data
</div>
</div>
</main>
</div>
<footer
class="mx_AuthFooter"
role="contentinfo"
>
<a
href="https://matrix.org"
rel="noreferrer noopener"
target="_blank"
>
powered by Matrix
</a>
</footer>
</div>
</div>
`;
exports[`<MatrixChat /> with an existing session onAction() room actions leave_room for a room should launch a confirmation modal 1`] = ` exports[`<MatrixChat /> with an existing session onAction() room actions leave_room for a room should launch a confirmation modal 1`] = `
<div <div
aria-describedby="mx_Dialog_content" aria-describedby="mx_Dialog_content"